# 一、网络程序设计技术

# 1. 网络程序设计基本概念

# 1. 1 网络体系结构

# 三种结构模型比较

# 2.Windows Sockets

# 2.1Winsock2 需要的头文件

winsock2.h、ws2_32.lib、ws2_32.dll

# 2.2WinSock 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int WSAAPI WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
/*
*加载相应的WinSock的dll版本。在使用WinSock函数前,若没有加载WinSock库,则函数就会返回一个SOCKET_ERROR,错误信息是WSANOTINITIALISED 。
加载Winsock库是通过调用WSAStartup函数实现的。
*/
/*
*WORD wVersionRequested [输入] 用于指定准备加载的Wi n s o c k的dll的版本,这个是调用者希望得到支持的最高的版本。高位字节指定所需要的Winsock库的副版本,而低位字节则是主版本。然后,可用宏MAKEWORD(X,Y)(其中,x 是主版本,y 是副版本)方便地获得WVersionRequested的正确值。如:winsock 2.2版本,可以用:MAKEWORD(2,2)或0x0202。
*/
/*
*lpWSAData[输出] 是指向LPWSADATA 结构的指针,目的是用于获取Winsock具体实现的更多的信息。定义如下:(在winsock2.h中定义)
*/
typedef struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
} WSADATA, FAR * LPWSADATA;

/*
*返回值:若成功,则返回0.否则返回非零。
若程序运行结束,则在结束之前,一定要使用int WSACleanup(void);来卸载所加载的DLL库。WSACleanup()调用运行时即使有错也没有特别大的关系。

*/

# 2.3Winsock API 的错误检查与处理

1
2
3
4
int WSAGetLastError(void);
/*
*一旦Winsock的函数表明发生了错误,则必须立即调用该函数。因为有些Winsock函数若操作成功会置最新的操作错误代码为0.
*/

# 2.4IPv4 地址的表示

IPv4 是一个 32 位的二进制数

1
2
3
4
5
6
7
struct sockaddr_in
{
short sin_family;//协议家族
u_short sin_port;//端口号
struct in_addr sin_addr;//IPv4网络地址
char sin_zero[8];
}

  1. sin_family
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #define AF_UNIX   1     //local to host(pipes,portals)
    #define AF_INET 2 // internetwork: UDP, TCP, etc.
    #define AF_IMPLINK 3 // arpanet imp addresses
    #define AF_PUP 4 // pup protocols: e.g. BSP
    #define AF_CHAOS 5 // mit CHAOS protocols
    #define AF_NS 6 // XEROX NS protocols
    #define AF_IPX AF_NS // IPX protocols: IPX, SPX, etc.
    #define AF_ISO 7 // ISO protocols
    #define AF_OSI AF_ISO // OSI is ISO
    #define AF_ECMA 8 //european computermanufacturers
    #define AF_DATAKIT 9 // datakit protocols
    #define AF_CCITT 10 // CCITT protocols, X.25 etc
    #define AF_SNA 11 // IBM SNA
    #define AF_DECnet 12 // DECnet
    #define AF_DLI 13 // Direct data link interface
    #define AF_LAT 14 // LAT
    #define AF_HYLINK 15 // NSC Hyperchannel
    #define AF_APPLETALK 16 // AppleTalk
    #define AF_NETBIOS 17 // NetBios-style addresses
    #define AF_VOICEVIEW 18 // VoiceView
    #define AF_FIREFOX 19 // Protocols from Firefox
    #define AF_UNKNOWN1 20 // Somebody is using this!
    #define AF_BAN 21 // Banyan
    #define AF_ATM 22 // Native ATM Services
    #define AF_INET6 23 // Internetwork Version 6
    #define AF_CLUSTER 24 // Microsoft Wolfpack
    #define AF_12844 25 // IEEE 1284.4 WG AF
    #define AF_MAX 26
  2. sin_port 是网络字节次序的端口号
  3. sin_addr 是网络字节次序的 IPv4 的 32 位地址表示
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    struct in_addr
    {
    union
    {
    struct{u_char s_b1,s_b2,s_b3,s_b4;}S_un_b;
    struct{u_short s_w1,s_w2;}S_un_w;
    u_long S_addr;
    }S_un;
    #define s_host S_un.S_un_b.s_b2 /* host on imp */
    #define s_net S_un.S_un_b.s_b1 /* network */
    #define s_imp S_un.S_un_w.s_w2 /* imp */
    #define s_impno S_un.S_un_b.s_b4 /* imp # */
    #define s_lh S_un.S_un_b.s_b3 /* logical host */
    }
    //IP地址转换函数
    unsigned long inet_addr(const char* cp);//功能:将“192.168.0.200”点分十进制表示法转换成32位的二进制数(IPV4)。
    char* FAR inet_ntoa(struct in_addr in);//功能:将struct in_addr 所表示的32位的二进制数(IPV4)地址转换成如“192.168.0.200”这样的点分十进制表示法的IPV4地址。

# 2.5 主机字节顺序与网络字节顺序

主机字节顺序采用的方式有区别
Internet规定的网络字节顺序采用大端方式

1
2
3
4
5
6
u_long  htonl(u_long hostlong);
u_short htons(u_short hostshort);
u_long ntohl(u_long netlong);
u_short ntohs(u_short netshort);
以上4个函数中h代表host,n代表network,s代表short,l代表long。Short是16位整数,long32位整数。前两个函数将主机字节顺序转换成网络字节顺序,而后两个函数则刚好相反。编程中,在需要使用网络字节顺序时,应该使用这几个函数来进行转换,绝对不要依赖于具体机器的表示方式。

# 2.6 其余辅助函数

  1. 获取主机名:gethostname ()
    1
    2
    3
    4
    5
    6
    7
    /*
    功能:返回本地主机的标准主机名
    参数:主机名
    成功返回0
    使用前WSAStartup()
    */
    int gethostname(char*name,int namelen);
  2. 返回主机名对应的主机信息:gethostbyname ()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /*
    返回值:若成功,则返回一个指向hostent的指针,否则返回:NULL。
    注:1)必须首先使用:WSAStartup()。
    2)所返回的结构的空间是在Winsock中分配的。不允许释放该结构中任何部分的指针空间。且若要一直使用,则首先在调用其它winsock函数之前,将它的内容保存到自己的变量中。
    3)点分十进制表示的IP地址的串,是不能解析成功的。应先使用inet_addr()将其转换成真正IP地址(struct in_addr),然后使用gethostbyaddr()转成hostent结构。
    */
    struct hostent* FAR gethostbyname(const char* name);

    typedef struct hostent
    {
    char FAR* h_name; //主机名
    char FAR FAR** h_aliases; //主机别名列表
    short h_addrtype; //地址类型
    short h_length; //地址长度
    char FAR FAR** h_addr_list;//主机的IP地址列表
    }hostnet;
  3. 返回对应一个 IP 网络地址的主机的信息:gethostbyaddr ()
    1
    struct hostent*FAR gethostbyaddr(const char* addr,int len,int type);

# 2.7 传输控制协议 (TCP)

# 2.7.1TCP 的 C/S 通信模型

* 服务端

  1. 创建 Socket
  2. 绑定地址
  3. 监听连接请求
  4. 接受请求
  5. 使用 send () 和 recv () 进行通信
  6. 关闭连接

客户端

  1. 创建 Socket
  2. 连接到服务器
  3. 使用 send () 和 recv () 进行通信
  4. 关闭连接

# 2.7.2 基本套接字函数

  1. socket 函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /*
    功能:创建一个未绑定的socket。SOCKET定义为u_int.
    参数:1)af:[输入] 指定要创建的套接字的协议簇;TCP/IP协议使用:AF_INET
    2)type:[输入]要创建的套接字类型:
    字节流型:SOCK_STREAM
    数据报型:SOCK_DGRAM
    原始SOCKET:SOCK_RAW
    3)protocol:[输入]指定使用哪种协议。通常设置为0,表示使用该类型SOCKET的默认协议。对字节流型的SOCK_STREAM意味着是TCP,对数据报型的SOCK_DGRAM意味着是UDP,对原始SOCKET的SOCK_RAW则必须指明(如:ICMP或IGMP等),因为此处填写的值将直接写入IP包的包头中。
    返回值:若成功则返回一个正整数,称为套接字描述符,标识这个套接字;否则,返回INVALID_SOCKET(该值不是-1)
    */
    SOCKET WSAAPI socket(int af,int type,int protocol);
  2. bind 函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /*
    功能:将本地地址和端口号与套接字绑定在一起。
    参数:
    1)s:[输入]要绑定的套接字描述符;
    2)name:[输入]实际上是使用struct sockaddr_in的变量的地址。在其中填写地址与端口号。
    3)namelen:[输入]套接字地址结构的长度。
    返回值:若成功则返回0;否则返回SOCKET_ERROR(-1)。
    */
    int bind(SOCKET s ,const struct sockaddr* name,int namelen);
    //注意是使用:struct sockaddr_in myaddr;
    //而不是:struct sockaddr myaddr;

    绑定操作一般有以下方式
程序类型IP 地址端口号说明
服务器INADDR_ANY非零值指定服务器的公认端口号
服务器本机 IP 地址非零值指定服务器的 IP 地址和公认端口号
客户机INADDR_ANY非零值指定客户机的连接端口号
客户机本地 IP 地址非零值指定客户机的 IP 地址和连接端口号
客户机本地 IP 地址指定客户机的 IP 地址

1
2
3
4
5
6
7
8
9
10
11
1. **服务器指定套接字的公认端口号,不指定IP地址。**
服务器调用函数bind时,如果设置套接字的IP地址为特殊的INADDR_ANY,表示它愿意接收来自任何网络设备接口的客户机连接。这是服务器最经常使用的绑定方式。发送时,源IP是默认输出端口网卡的IP。这是多网卡的服务器上常用的方式。
2. **服务器指定套接字的公认端口号和IP地址。**
服务器调用函数bind时,如果设置套接字的IP地址为某个本地IP地址,这表示服务器只接收来自对应于这个IP地址的特定网络设备接口的客户机连接。如果这台机器只有一个网络设备接口,这和第一种情况是没有区别的,但当这台机器有多个网络设备接口时,我们可以用这种方式来限制服务器的接收范围。
3. **客户机指定套接字的连接端口号,不指定地址(地址为INADDR_ANY)**
在一般情况下,客户机不用指定自己的套接字的端口号,当客户机调用函数connect进行TCP连接时,系统会自动为它选择一个未用的端口号,并且用本地的IP地址来填充套接字地址中的相应项。但在有的情况下,客户机需要使用特定端口号,如Linux系统中的rlogin命令(见RFC1282),因为rlogin命令需要使用保留端口号,而系统不会为客户机自动分配一个保留端口号,所以需要调用函数bind来和一个未用的保留端口号绑定。
4. **指定客户机的IP地址和连接端口号。**
表示客户机使用指定的网络设备接口和端口号进行通信。
5. **指定客户机的IP地址,不指定端口号。**
表示客户机使用指定的网络设备接口进行通信,系统自动为客户机选择一个未用的端口号(此时在:1024到5000之间自动选择一个)。一般只有在主机有多个网络设备接口时使用。
注:在编写客户机程序时,一般不要使用固定的客户机端口号,除非是在必须使用特定端口号的特殊情况下。

  1. listen 函数

    1
    2
    3
    4
    5
    6
    7
    8
    /*
    功能:将一个已绑定的套接字转换为倾听套接字。
    参数:
    1) sockfd:[输入]指定要转换的套接字描述符;
    2) backlog:[输入]设置请求队列的最大长度(处于等待建立TCP全连接的请求,通常是半打开的TCP连接)。
    返回值:成功则返回0;否则返回SOCKET_ERROR(-1)。
    */
    int listen(SOCKET sockfd,int backlog);

    服务器需要调用函数 listen 将套接字转换成倾听套接字,以便接收客户机请求。函数 listen 的功能有两个:
    (1) 函数 socket 创建的套接字是主动套接字,可以用它来进行主动连接 (调用函数 connect),但是不能接收连接请求,而服务器的套接字必须能够接收客户机的请求。函数 listen 将一个尚未连接的主动套接字转换成为 — 个被动套接字:告诉 TCP 协议,这个套接字可以接收连接请求。执行函数 listen 之后,服务器的 TCP 状态由 CLOSED 状态转换成 LISTEN 状态。
    (2) TCP 协议将到达的连接请求排队,函数 listen 的第二个参数指定这个队列的最大长度。 要创建一个倾听套接字,必须首先调用函数 socket 创建一个主动套接字,然后调用函数 bind 将它与服务器套接宇地址绑定在一起,最后调用函数 listen 进行转换。这 3 步操作是所有 TCP 服务器所必须的操作。若该请求队列已满,但又有新的请求到达时,则新的请求将被拒绝。backlog 的最大值由具体实现指定。若程序员指定的值超过该值,则系统会自动选择一个最接近用户值但系统又能支持的值。

  2. accept 函数

1
2
3
4
5
6
7
8
9
10
11
SOCKET accept(SOCKET sockfd,struct sockaddr *addr,int *addrlen);
/* 功能:从倾听套接字的已完成连接队列中接收—个连接。
参数:
1) sockfd:[输入]等待连接的倾听套接字描述符;
2) addr:[输出]为指向一个Internet套接字地址结构的指针(即 struct sockaddr_in *);用于存放远方本次连接的IP地址与端口号。若对该信息不感兴趣,则可置为:NULL。
3) addrlen:[输入/输出]:为指向一个整型变量的指针。输入时,该指针变量所指的内容为:addr的空间长度。输出时为远方IP地址的长度。当addr为NULL时,此域为NULL。

返回值:函数accept成功执行时,返回3个结果:
函数返回值为一个新的套接字描述符,标识这个接收的连接;参数addr指向的结构变量中存储客户机地址与端口号;参数addlen指向的整型变量中存储客户机地址的长度。如果对客户机的地址和长度不感兴趣,可以将参数addr和addrlen设置为NULL。函数accept执行失败时,返回 INVALID_SOCKET(定义这~0,即非0).

*/

  1. closesocket 函数

1
2
3
4
5
int closesocket(SOCKET  sockfd)
/*功能:关闭一个已存在的套接字。释放相关的资源。
参数:sockfd:[输入]指定要关闭的套接字。
返回值:函数closesocket成功执行时,返回0;否则,返回-1(SOCKET_ERROR)。
套接字描述符的closesocket操作和文件描述符的closesocket e操作一样:函数close*/

  1. connect 函数

1
2
3
4
5
6
7
8
int connect(SOCKET sockfd,struct sockaddr *servaddr, int addrlen)
/*功能:用于客户机向服务器发起一个连接。
参数:
1) sockfd:[输入]客户机自己的socket套接字;
2) servaddr:[输入]在TCP/IP下,是远程服务器的struct sockaddr_in结构的地址,在其中包含服务器的IP地址和端口号;
3) addrlen:[输入]servaddr所指的struct sockaddr_in结构的长度。
返回值:成功则返回O;否则返回SOCKET_ERROR(-1)
*/

# 2.7.3 网络数据传输

  1. send 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int WSAAPI send(
SOCKET s,
const char FAR * buf,
int len,
int flags
);
/*功能:在已建立连接的套接字上发送数据。
参数:
1)s:[输入] 已建立连接的套接字,将在这个套接字上发送数据。
2)buf:[输入] 字符缓冲区,其中包含即将发送的数据。
3)len :[输入]指定即将发送的缓冲区内的字符数。
4)flags:[输入] 可为0 、M S G _ D O N T R O U T E 或M S G _ O O B 。另外,f l a g s 还可以是对那些标志进行按位“或运算”的一个结果。M S G _ D O N T R O U T E 标志要求传送层不要将它发出的包路由出去。由基层的传送决定是否实现这一请求(例如,若传送协议不支持该选项,这一请求就会被忽略)。M S G _ O O B 标志预示数据应该被带外发送。
返回值:返回实际发送的字节数;若发生错误,就返回S O C K E T _ E R R O R 。
*/

  1. recv 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int WSAAPI recv(
SOCKET s,
char FAR * buf,
int len,
int flags
);
/*功能:在已连接套接字上接受数据。
参数:
1) s:[输入]准备接收数据的那个套接字。
2) b u f :[输出]即将收到数据的字符缓冲区。
3) l e n:[输入] 准备接收的字节数或b u f 缓冲的长度。
4) f l a g s :[输入]可以是下面的值:0 、M S G _ P E E K 或M S G _ O O B 。另外,还可对这些标志中的每一个进行按位和运算。当然,0 表示无特殊行为。M S G _ P E E K 会使有用的数据复制到所提供的接收端缓冲内,但是没有从系统缓冲中将它删除。
返回值:返回已接受的字节数。若socket已关闭,则返回0,若出错返回:SOCKET_ERROR。
*/

# TCP 服务器设计

S1、调用 WSAStartup()装载 Winsock 相应版本的 DLL 库。
S2、调用 socket () 创建一个 socket。
S3、调用 bind () 绑定服务器的 IP 和 PORT。
S4、调用 listen () 变成倾听 socket。
S5、调用 accept () 等待客户机的连接。
S6、调用 send ()/recv () 按应用协议进行网络通信。
S7、调用 closesocket () 关闭相应的 socket。

# TCP 客户机设计

S1、调用 WSAStartup()装载 Winsock 相应版本的 DLL 库。
S2、调用 socket () 创建一个 socket。
S3、调用 connect 向服务器发起一个 TCP 连接。
S6、调用 send ()/recv () 按应用协议进行网络通信。
S7、调用 closesocket () 关闭相应的 socket。

# 2.8 用户数据流协议 (UDP)

# 2.8.1UDP 建立

1.UDPSOCKET 建立

1
SOCKET udps=socket(AF_INET,SOCKE_DGRAM,0);

  1. UDP 数据报的发送 sendto ()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int sendto(
SOCKET s,
const char* buf,
int len,
int flags,
const struct sockaddr* to,
int tolen
);
/*功能:用于发送一个UDP包。
参数: 1) s:[输入]要发送UDP包的socket。
2) buf:[输入]要发送的数据的缓冲区。
3) len:[输入]要发送的数据的字节数。
4) flags:[输入] 可为0 、M S G _ D O N T R O U T E 或M S G _ O O B 。另外,f l a g s 还可以是对那些标志进行按位“或运算”的一个结果。M S G _ D O N T R O U T E 标志要求传送层不要将它发出的包路由出去。由基层的传送决定是否实现这一请求(例如,若传送协议不支持该选项,这一请求就会被忽略)。M S G _ O O B 标志预示数据应该被带外发送。
5)to:[输入]接受方的IP地址与端口号。
6) tolen:[输入]参数to所指的数据区的大小。
*/

3. 接受 UDP 包 recvfrom ()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int recvfrom(
SOCKET s,
char* buf,
int len,
int flags,
struct sockaddr* from,
int* fromlen
);
/*功能:接受一个UDP包。
参数:
1) s:[输入]准备接收UDP包的那个套接字。
2) b u f :[输出]即将收到数据的字符缓冲区。
3) l e n:[输入] 准备接收的字节数或b u f 缓冲的长度。
4) f l a g s :[输入]可以是下面的值:0 、M S G _ P E E K 或M S G _ O O B 。另外,还可对这些标志中的每一个进行按位和运算。当然,0 表示无特殊行为。M S G _ P E E K 会使有用的数据复制到所提供的接收端缓冲内,但是没有从系统缓冲中将它删除。
5)from:[输出]发送方的地址与端口号。
6) fromlen:[输入]from所指数据区的大小。
*/

# 2.8.2UDP 丢包

数据大小超过 64K

# 2.9MTU (最大可传输单元)

1
2
3
4
5
6
7
8
9
一个数据包穿过一个大的网络,它其间会穿过多个网络,每个网络的 MTU 值是不同的。这个网络中最小的 MTU 值,被称为路径 MTU。

假设:我们的接受/发送端都是以太网,它们的 MTU 都是 1500,我们发送的时候,数据包会以 1500 来封装,然而,不幸的是,传输中有一段X.25网,它的 MTU 是 576,这会发生什么呢?

结论是显而易见的,这个数据包会被再次分片,更重要的是,这种情况下,如果 IP 包被设置了“不允许分片标志”,那会发生些什么呢?

对,数据包将被丢弃,然事收到一份ICMP不可达差错,告诉你,需要分片!

很显然,MTU 值设置得过大或过小,都会在一定程度上影响我们上网的速度。

# 2.10IP 数据包

# TCP 的使用场景:

  1. 可靠性要求高: 当应用程序需要可靠的、面向连接的数据传输时,通常选择 TCP。TCP 提供数据的可靠性,确保数据按顺序到达,并处理丢失的数据包的重传。
  2. 顺序性要求高: 如果应用程序需要确保数据按发送顺序到达,TCP 是更好的选择。TCP 使用序列号来确保数据包按正确的顺序传递。
  3. 适用于大数据量传输: TCP 适用于需要传输大量数据的场景,因为它可以自动调整传输速率,通过拥塞控制和流量控制来保证数据的可靠传输。
  4. 网页浏览、文件传输: 在需要确保数据准确传输的场景下,如网页浏览、文件传输、电子邮件等应用中,通常使用 TCP。

# UDP 的使用场景:

  1. 实时性要求高: 当应用程序对实时性要求较高,可以容忍少量数据包的丢失而不进行重传时,通常选择 UDP。UDP 是一种无连接的协议,没有建立和断开连接的过程,因此具有较低的延迟。
  2. 广播和多播: UDP 支持广播和多播通信,适用于向多个主机同时发送相同的数据。
  3. 音视频传输: 在实时音视频通信中,如 VoIP、视频会议等,通常使用 UDP。虽然 UDP 不提供可靠性,但在实时应用中,实时性更为重要。
  4. 简单的请求 - 响应通信: 当通信模式为简单的请求 - 响应模式,而且可以容忍一定的数据丢失时,UDP 是一个合适的选择。