0%

网络编程

网络编程

网络基础知识

网络OSI模型

网络OSI7层模型(Open Systems Interconnection)即开放系统互联。它是由ISO(Internet Organization for Standardization)制定

OSI时网络通讯的一种通用框架,它分为七层

序号
7 应用层(Application)
6 表示层(Presentation)
5 会话层(Session)
4 传输层(Transport)->TCP/UDP
3 网络层(NetWork)
2 数据链路层(Data Link)
1 物理层(Physical)

但不是所有网络通信都必须经过这7层模型,比如同一网段下两台计算机的交互,只需要经过物理层和数据链路层就够了。

网络上最重要的就是IP,IP就在网络层。

Windows Socket

什么是套接字(Socket)

Socket又称套接字,它是TCP/IP网络环境下应用程序与底层通信驱动程序之间运行的开发接口.它可以将应用程序与具体的TCP/IP隔离开.使得应用程序不需要了解TCP/IP的细节,就能够实现传输。

套接字的服务方式和类型

根据底层协议的不同,Socket开发接口可以提供面向连接和无连接二种服务方式。

在Socketi通信中,套接字分为3种类型。

1.流式套接字.SOCK_STREAM

流式套接字是面向连接的、提供双向、有序、无重复且无记录边界的数据流服务,适用于处理大量数据,可靠性高,但开销也大。

2.数据报式套接字SOCK_DGRAM

UDP,一般应用在传输音视频文件,容许少量丢包

3.原始套接字

其中SOCK_STREAM、SOCK_DGRAM工作在传输层,SOCK_RAW工作在网络层。

构建WinSock应用程序框架

WinSock包含了两个主要的版本,即Socket1和winsock2,我们一般使用2
在使用时,需要包含如下头文件和lib文件
#include <WinSock2.h>
#pragma comment(lib,”ws2_32.lib”)

还需要初始化WinSocket
Int WSAAPI WSAStartup(WORD wVersionRequested //版本号,LPWSADATA IpWSAData); WSAData结构用于存放返回的Socket数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")

int main()
{
WORD wVersion = MAKEWORD(2, 2); //版本号
WSADATA wsaData; // 一个结构体,用来返回数据
if (WSAStartup(wVersion, &wsaData))
{
printf("WSAStartup failed");
return 3;
}
else
{
printf("WSAStartup successed");
}

return 2;
}

WSAData,存放Winsock返回的初始化信息:

1
2
3
4
5
6
7
8
9
typedef struct WSAData {
WORD wVersion; // Socket DLL期望用户使用的规范版本
WORD wHighVersion; // 可以支持的最高版本
char szDescription[WSADESCRIPTION_LEN+1]; // 描述字符串
char szSystemStatus[WSASYSSTATUS_LEN+1]; // 状态字符串
unsigned short iMaxSockets; // 套接字的最大编号(WinSock2或稍后的版本忽略了该字段)
unsigned short iMaxUdpDg; // 忽略
char FAR * lpVendorInfo; // 废弃
};

image-20240708231715068

IP地址的表现形式

IP地址常用点分法来表示比如192.168.0.1,即4个0-255的整数表示

但是在计算机中不使用点分法来保存IP地址.这样会浪费存诸空间.而且不便于根据IP地址和子网掩码来计算子网信息
所以在计算机中使用无符号长整形数来存诸表示IP地址
1.网络字节顺序(NetWork Byte Order,NBO)
在网络传送中,IP地址被保存为32位的二进制数在低位存储地址中保存数据的高位字节这种存储顺序格式被称为网络字节顺序.数据按照32位的二进制数为一组进行传输。因为采用网络字节顺序,所以数据的传输顺序是由高到低的。
在VC中使用In_addr来保存IP地址。 inet_addr和 inet_ntoa

in_addr:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct in_addr {
union {
struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b; //点分法保存
struct { USHORT s_w1,s_w2; } S_un_w; //分两部分保存
ULONG S_addr; //保存为32位
} S_un;
#define s_addr S_un.S_addr /* can be used for most tcp & ip code */
#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
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;

image-20240709130551236

2.主机字节顺序(Host Byte Order,HBO)两种格式
不同的主机在对IP地址进行存储时使用的格式也不同,所以需要通过下列四个函数
来实现主机和网络字节顺序的互转
htonl 将主机字节顺序格式IP地址转换成为TCP/IP网络字节顺序 Host to Net u_long
htons主机转网络u_short型
ntohl网络转主机 u_long Net to Host u_long
ntohs网络转主机 u_short

WinSock TCP/IP连接流程

服务端 客户端
1.建立流式套接字 1.建立流式套接字
2.套接字:与本地地址绑定
3.通知TCP服务器准备连接
4.等待客户端的连接 2.将套接字与远程主机连接
5.建立连接
6.在套接字上读写数据 3.在套接字上读写数据
7.关闭套接字 4.关闭套接字结束对话
8.关闭最初的套接字,结束服务

image-20240709205503926

地址族:

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
29
30
#define AF_UNSPEC       0               // unspecified
#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 computer manufacturers
#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_IRDA 26 // IrDA
#define AF_NETDES 28 // Network Designers OSI & gateway

套接字类型:

1
2
3
4
5
#define SOCK_STREAM     1               /* stream socket */
#define SOCK_DGRAM 2 /* datagram socket */
#define SOCK_RAW 3 /* raw-protocol interface */
#define SOCK_RDM 4 /* reliably-delivered message */
#define SOCK_SEQPACKET 5 /* sequenced packet stream */

套接字使用的协议:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
typedef enum {
#if(_WIN32_WINNT >= 0x0501)
IPPROTO_HOPOPTS = 0, // IPv6 Hop-by-Hop options
#endif//(_WIN32_WINNT >= 0x0501)
IPPROTO_ICMP = 1,
IPPROTO_IGMP = 2,
IPPROTO_GGP = 3,
#if(_WIN32_WINNT >= 0x0501)
IPPROTO_IPV4 = 4,
#endif//(_WIN32_WINNT >= 0x0501)
#if(_WIN32_WINNT >= 0x0600)
IPPROTO_ST = 5,
#endif//(_WIN32_WINNT >= 0x0600)
IPPROTO_TCP = 6,
#if(_WIN32_WINNT >= 0x0600)
IPPROTO_CBT = 7,
IPPROTO_EGP = 8,
IPPROTO_IGP = 9,
#endif//(_WIN32_WINNT >= 0x0600)
IPPROTO_PUP = 12,
IPPROTO_UDP = 17,
IPPROTO_IDP = 22,
#if(_WIN32_WINNT >= 0x0600)
IPPROTO_RDP = 27,
#endif//(_WIN32_WINNT >= 0x0600)

#if(_WIN32_WINNT >= 0x0501)
IPPROTO_IPV6 = 41, // IPv6 header
IPPROTO_ROUTING = 43, // IPv6 Routing header
IPPROTO_FRAGMENT = 44, // IPv6 fragmentation header
IPPROTO_ESP = 50, // encapsulating security payload
IPPROTO_AH = 51, // authentication header
IPPROTO_ICMPV6 = 58, // ICMPv6
IPPROTO_NONE = 59, // IPv6 no next header
IPPROTO_DSTOPTS = 60, // IPv6 Destination options
#endif//(_WIN32_WINNT >= 0x0501)

IPPROTO_ND = 77,
#if(_WIN32_WINNT >= 0x0501)
IPPROTO_ICLFXBM = 78,
#endif//(_WIN32_WINNT >= 0x0501)
#if(_WIN32_WINNT >= 0x0600)
IPPROTO_PIM = 103,
IPPROTO_PGM = 113,
IPPROTO_L2TP = 115,
IPPROTO_SCTP = 132,
#endif//(_WIN32_WINNT >= 0x0600)
IPPROTO_RAW = 255,

IPPROTO_MAX = 256,
//
// These are reserved for internal use by Windows.
//
IPPROTO_RESERVED_RAW = 257,
IPPROTO_RESERVED_IPSEC = 258,
IPPROTO_RESERVED_IPSECOFFLOAD = 259,
IPPROTO_RESERVED_WNV = 260,
IPPROTO_RESERVED_MAX = 261
} IPPROTO, *PIPROTO;

代码

服务端

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

#define PORT 8080
#define BUFSIZE 1024

int main() {
WSADATA wsaData;
int iResult;
SOCKET server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFSIZE] = {0};
const char *hello = "Hello from server";

// 初始化 Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}

// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
perror("socket failed");
WSACleanup();
return 1;
}

// 绑定socket到端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) == SOCKET_ERROR) {
perror("bind failed");
closesocket(server_fd);
WSACleanup();
return 1;
}

// 监听
if (listen(server_fd, 3) == SOCKET_ERROR) {
perror("listen");
closesocket(server_fd);
WSACleanup();
return 1;
}

// 接受新连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen)) == INVALID_SOCKET) {
perror("accept");
closesocket(server_fd);
WSACleanup();
return 1;
}

// 读取客户端发送的数据
iResult = recv(new_socket, buffer, BUFSIZE, 0);
if (iResult > 0) {
printf("Received: %s\n", buffer);
} else if (iResult == 0) {
printf("Connection closing...\n");
} else {
perror("recv failed");
closesocket(new_socket);
closesocket(server_fd);
WSACleanup();
return 1;
}

// 发送数据到客户端
send(new_socket, hello, strlen(hello), 0);
printf("Hello message sent\n");

// 关闭socket
closesocket(new_socket);
closesocket(server_fd);
WSACleanup();

return 0;
}

客户端

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

#define PORT 8080
#define BUFSIZE 1024

int main() {
WSADATA wsaData;
int iResult;
SOCKET client_fd;
struct sockaddr_in serv_addr;
char buffer[BUFSIZE] = {0};
const char *hello = "Hello from client";

// 初始化 Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}

// 创建socket文件描述符
if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
perror("socket failed");
WSACleanup();
return 1;
}

// 设置服务器地址
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 将IP地址从字符串转换为网络地址
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("inet_pton failed");
closesocket(client_fd);
WSACleanup();
return 1;
}

// 连接到服务器
if (connect(client_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR) {
perror("connect failed");
closesocket(client_fd);
WSACleanup();
return 1;
}

// 发送数据到服务器
send(client_fd, hello, strlen(hello), 0);
printf("Hello message sent\n");

// 读取服务器发送的数据
iResult = recv(client_fd, buffer, BUFSIZE, 0);
if (iResult > 0) {
printf("Received: %s\n", buffer);
} else if (iResult == 0) {
printf("Connection closed\n");
} else {
perror("recv failed");
}

// 关闭socket
closesocket(client_fd);
WSACleanup();

return 0;
}

面向非连接的SOCKET编程

特点

面向非连接(UDP)的特点:

1.不需要在服务端和客户端之间进建立连接

2.不对收到的数据进行排序

3.对接收到的数据包不发送确认信息,发送端无法知道数据是否被正确接收,也不会重新发送数据

4.传送数据较TCP快,系统开销也少

UDP连接流程

主机流程(UDP) 客户端流程
1.建立流式套接字 1.建立流式套接字
2.套接字:与本地地址绑定
3.在套接字上读/写数据 2.在套接字上读/写数据
4.关闭套接字 3.关闭套接字

代码

服务端

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

#define PORT 8080
#define BUFSIZE 1024

int main() {
WSADATA wsaData;
int iResult;
SOCKET server_fd;
struct sockaddr_in address, client_addr;
int addrlen = sizeof(client_addr);
char buffer[BUFSIZE] = {0};
const char *hello = "Hello from server";

// 初始化 Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}

// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) == INVALID_SOCKET) {
perror("socket failed");
WSACleanup();
return 1;
}

// 绑定socket到端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) == SOCKET_ERROR) {
perror("bind failed");
closesocket(server_fd);
WSACleanup();
return 1;
}

// 接收数据
iResult = recvfrom(server_fd, buffer, BUFSIZE, 0, (struct sockaddr *)&client_addr, &addrlen);
if (iResult > 0) {
printf("Received: %s\n", buffer);
} else {
perror("recvfrom failed");
closesocket(server_fd);
WSACleanup();
return 1;
}

// 发送数据
sendto(server_fd, hello, strlen(hello), 0, (struct sockaddr *)&client_addr, addrlen);
printf("Hello message sent\n");

// 关闭socket
closesocket(server_fd);
WSACleanup();

return 0;
}

客户端

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

#define PORT 8080
#define BUFSIZE 1024

int main() {
WSADATA wsaData;
int iResult;
SOCKET client_fd;
struct sockaddr_in serv_addr;
char buffer[BUFSIZE] = {0};
const char *hello = "Hello from client";

// 初始化 Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}

// 创建socket文件描述符
if ((client_fd = socket(AF_INET, SOCK_DGRAM, 0)) == INVALID_SOCKET) {
perror("socket failed");
WSACleanup();
return 1;
}

// 设置服务器地址
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 将IP地址从字符串转换为网络地址
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("inet_pton failed");
closesocket(client_fd);
WSACleanup();
return 1;
}

// 发送数据到服务器
sendto(client_fd, hello, strlen(hello), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
printf("Hello message sent\n");

// 接收服务器发送的数据
iResult = recvfrom(client_fd, buffer, BUFSIZE, 0, NULL, NULL);
if (iResult > 0) {
printf("Received: %s\n", buffer);
} else {
perror("recvfrom failed");
}

// 关闭socket
closesocket(client_fd);
WSACleanup();

return 0;
}

阻塞式模型

阻塞式

什么是阻塞式?

1.在创建一个套接字后,默认都是阻塞式的Winsocket的IO函数比如:Send和Recv,必须等待函数完成相应的I/O操作后,才能继续

什么是非阻塞式?

1.通过调用ioctlsocket(SOCKET s, long cmd, u_long *arpg)函数,改变该套接字的模式,
U_long nNoBlock =1;
loctlsocket(s,FIONBIO,&nNoBlock);

2.无论操作是否完成,非阻塞式函数都会立即返回。例如,在非阻塞模式下调用recv接收数据时,程序会直接读取网络缓冲区中的数据。无论是否读到数据,函数都会立即返回

阻塞式迭代模式

1.每次只服务一个连接,只有在服务完当前客户端连接之后,才会继续服务下一个客户端连接

阻塞式迭代模式步骤

1
2
3
4
5
6
7
8
9
10
// 1.先处理连接 绑定本地地址和监听
SOCKET Bind_Listen(int nBacklog)
// 2.接收一个客户端连接并返回对应的连接的套接字
SOCKET AcceptConnetion(SOCKET hSocket)
// 3.处理一个客户端的连接,实现接收和发送数据
BOOL ClientConFun(SOCKET sd)
// 4.关闭一个连接
BOOL CloseConnect(SOCKET sd)
// 5.服务器主体
void MyTepSerFun()

代码

服务端
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

#define PORT 8080
#define BUFSIZE 1024

// 1. 先处理连接 绑定本地地址和监听
SOCKET Bind_Listen(int nBacklog) {
WSADATA wsaData;
int iResult;
SOCKET server_fd;
struct sockaddr_in address;

// 初始化 Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return INVALID_SOCKET;
}

// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
perror("socket failed");
WSACleanup();
return INVALID_SOCKET;
}

// 绑定socket到端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) == SOCKET_ERROR) {
perror("bind failed");
closesocket(server_fd);
WSACleanup();
return INVALID_SOCKET;
}

// 监听
if (listen(server_fd, nBacklog) == SOCKET_ERROR) {
perror("listen");
closesocket(server_fd);
WSACleanup();
return INVALID_SOCKET;
}

return server_fd;
}

// 2. 接收一个客户端连接并返回对应的连接的套接字
SOCKET AcceptConnetion(SOCKET hSocket) {
struct sockaddr_in client_addr;
int addrlen = sizeof(client_addr);
SOCKET new_socket = accept(hSocket, (struct sockaddr *)&client_addr, &addrlen);
if (new_socket == INVALID_SOCKET) {
perror("accept");
}
return new_socket;
}

// 3. 处理一个客户端的连接,实现接收和发送数据
BOOL ClientConFun(SOCKET sd) {
char buffer[BUFSIZE] = {0};
const char *hello = "Hello from server";
int iResult;

// 接收数据
iResult = recv(sd, buffer, BUFSIZE, 0);
if (iResult > 0) {
printf("Received: %s\n", buffer);
} else if (iResult == 0) {
printf("Connection closing...\n");
return FALSE;
} else {
perror("recv failed");
return FALSE;
}

// 发送数据
send(sd, hello, strlen(hello), 0);
printf("Hello message sent\n");

return TRUE;
}

// 4. 关闭一个连接
BOOL CloseConnect(SOCKET sd) {
if (closesocket(sd) == SOCKET_ERROR) {
perror("closesocket");
return FALSE;
}
return TRUE;
}

// 5. 服务器主体
void MyTepSerFun() {
SOCKET server_fd = Bind_Listen(3);
if (server_fd == INVALID_SOCKET) {
return;
}

while (1) {
SOCKET client_socket = AcceptConnetion(server_fd);
if (client_socket != INVALID_SOCKET) {
ClientConFun(client_socket);
CloseConnect(client_socket);
}
}

closesocket(server_fd);
WSACleanup();
}

int main() {
MyTepSerFun();
return 0;
}
客户端
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

#define PORT 8080
#define BUFSIZE 1024

int main() {
WSADATA wsaData;
int iResult;
SOCKET client_fd;
struct sockaddr_in serv_addr;
char buffer[BUFSIZE] = {0};
const char *hello = "Hello from client";

// 初始化 Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}

// 创建socket文件描述符
if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
perror("socket failed");
WSACleanup();
return 1;
}

// 设置服务器地址
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 将IP地址从字符串转换为网络地址
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
perror("inet_pton failed");
closesocket(client_fd);
WSACleanup();
return 1;
}

// 连接到服务器
if (connect(client_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR) {
perror("connect failed");
closesocket(client_fd);
WSACleanup();
return 1;
}

// 发送数据到服务器
send(client_fd, hello, strlen(hello), 0);
printf("Hello message sent\n");

// 接收服务器发送的数据
iResult = recv(client_fd, buffer, BUFSIZE, 0);
if (iResult > 0) {
printf("Received: %s\n", buffer);
} else if (iResult == 0) {
printf("Connection closed\n");
} else {
perror("recv failed");
}

// 关闭socket
closesocket(client_fd);
WSACleanup();

return 0;
}

阻塞式并发连接模式

1.通过多线程,可以同时服务多个连接,每一个线程处理一个客户端连接

和迭代模式基本相同,只是在于处理客户端连接上,需要用到多线程处理多线程来处理客户端连接,以给予服务端同时处理业务的能力。

代码

主要是针对服务端的代码,所以这里只写服务端

服务端
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <process.h>

#pragma comment(lib, "ws2_32.lib")

#define PORT 8080
#define BUFSIZE 1024

// 线程函数,处理客户端连接
unsigned __stdcall ClientThread(void *arg) {
SOCKET client_socket = *(SOCKET *)arg;
char buffer[BUFSIZE] = {0};
const char *hello = "Hello from server";
int iResult;

// 接收数据
iResult = recv(client_socket, buffer, BUFSIZE, 0);
if (iResult > 0) {
printf("Received: %s\n", buffer);
} else if (iResult == 0) {
printf("Connection closing...\n");
} else {
perror("recv failed");
}

// 发送数据
send(client_socket, hello, strlen(hello), 0);
printf("Hello message sent\n");

// 关闭连接
closesocket(client_socket);
free(arg);
return 0;
}

// 绑定本地地址和监听
SOCKET Bind_Listen(int nBacklog) {
WSADATA wsaData;
int iResult;
SOCKET server_fd;
struct sockaddr_in address;

// 初始化 Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return INVALID_SOCKET;
}

// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
perror("socket failed");
WSACleanup();
return INVALID_SOCKET;
}

// 绑定socket到端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) == SOCKET_ERROR) {
perror("bind failed");
closesocket(server_fd);
WSACleanup();
return INVALID_SOCKET;
}

// 监听
if (listen(server_fd, nBacklog) == SOCKET_ERROR) {
perror("listen");
closesocket(server_fd);
WSACleanup();
return INVALID_SOCKET;
}

return server_fd;
}

// 服务器主体
void MyTepSerFun() {
SOCKET server_fd = Bind_Listen(3);
if (server_fd == INVALID_SOCKET) {
return;
}

while (1) {
struct sockaddr_in client_addr;
int addrlen = sizeof(client_addr);
SOCKET *client_socket = (SOCKET *)malloc(sizeof(SOCKET));
*client_socket = accept(server_fd, (struct sockaddr *)&client_addr, &addrlen);
if (*client_socket == INVALID_SOCKET) {
perror("accept");
free(client_socket);
continue;
}

// 创建线程处理客户端连接
_beginthreadex(NULL, 0, ClientThread, (void *)client_socket, 0, NULL);
}

closesocket(server_fd);
WSACleanup();
}

int main() {
MyTepSerFun();
return 0;
}

非阻塞式

代码

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <process.h>

#pragma comment(lib, "ws2_32.lib")

#define PORT 8080
#define BUFSIZE 1024

// 线程函数,处理客户端连接
unsigned __stdcall ClientThread(void *arg) {
SOCKET client_socket = *(SOCKET *)arg;
char buffer[BUFSIZE] = {0};
const char *hello = "Hello from server";
int iResult;

// 设置为非阻塞模式
u_long iMode = 1;
iResult = ioctlsocket(client_socket, FIONBIO, &iMode);
if (iResult != NO_ERROR) {
perror("ioctlsocket failed");
closesocket(client_socket);
free(arg);
return 1;
}

while (1) {
// 接收数据
iResult = recv(client_socket, buffer, BUFSIZE, 0);
if (iResult > 0) {
printf("Received: %s\n", buffer);
// 发送数据
send(client_socket, hello, strlen(hello), 0);
printf("Hello message sent\n");
} else if (iResult == 0) {
printf("Connection closing...\n");
break;
} else {
if (WSAGetLastError() == WSAEWOULDBLOCK) {
// 没有数据可读,继续循环
Sleep(100);
continue;
} else {
perror("recv failed");
break;
}
}
}

// 关闭连接
closesocket(client_socket);
free(arg);
return 0;
}

// 绑定本地地址和监听
SOCKET Bind_Listen(int nBacklog) {
WSADATA wsaData;
int iResult;
SOCKET server_fd;
struct sockaddr_in address;

// 初始化 Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return INVALID_SOCKET;
}

// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
perror("socket failed");
WSACleanup();
return INVALID_SOCKET;
}

// 绑定socket到端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) == SOCKET_ERROR) {
perror("bind failed");
closesocket(server_fd);
WSACleanup();
return INVALID_SOCKET;
}

// 监听
if (listen(server_fd, nBacklog) == SOCKET_ERROR) {
perror("listen");
closesocket(server_fd);
WSACleanup();
return INVALID_SOCKET;
}

return server_fd;
}

// 服务器主体
void MyTepSerFun() {
SOCKET server_fd = Bind_Listen(3);
if (server_fd == INVALID_SOCKET) {
return;
}

while (1) {
struct sockaddr_in client_addr;
int addrlen = sizeof(client_addr);
SOCKET *client_socket = (SOCKET *)malloc(sizeof(SOCKET));
*client_socket = accept(server_fd, (struct sockaddr *)&client_addr, &addrlen);
if (*client_socket == INVALID_SOCKET) {
if (WSAGetLastError() == WSAEWOULDBLOCK) {
// 没有连接请求,继续循环
Sleep(100);
continue;
} else {
perror("accept");
free(client_socket);
continue;
}
}

// 创建线程处理客户端连接
_beginthreadex(NULL, 0, ClientThread, (void *)client_socket, 0, NULL);
}

closesocket(server_fd);
WSACleanup();
}

int main() {
MyTepSerFun();
return 0;
}

Select模型

Select模型对应的是一个函数

从字面意思理解,select可从诸多连接中检测出可读的(accpet函数),也就是有响应的连接;也可以从诸多连接中检测出可写的(recv、send函数),也就是可以发送消息的连接。

image-20240717223748067

select模型逻辑

select模型逻辑步骤如下:

  1. 将所有的socket装进一个数组中
  2. 通过select函数遍历socket数组,取出有响应(可读、可写)的socket放进另一个数组
  3. 对存入有响应的socket数组处理

函数和结构体

Select函数:

1
2
3
4
5
6
7
int select(
int nfds, //直接为0
fd_set *readfds, //套接字集合,针对读操作accept, recv
fd_set *writefds,//针对写操作,如connect send等
fd_set *exceptfds,//针对异常
struct timeval *timeout//超时设置,为NULL就是一直等
);

返回值 1,如果超时,返回值 0,出现错误,返回值Socket_ERROR

fd_set结构体

1
2
3
4
5
typedef struct fd_set{
u_int fd_count; /* How many SET */
SOCKET fd_array[FD_SETSIZE]; /* An array of SOCKETs*/
}fd_set;
#define FDZ_SETSIZE 64

因为#define FDZ_SETSIZE 64所以最大不能超过64个,代表着可以并发处理64个套接字

如果想装更多的socket,可以通过在winsock2.h头文件前声明宏,给一个更大的值:

1
2
#define FD_SETSIZE 128
#include <WinSock2.h>

四个操作fd_set的操作宏

操作宏 作用 代码
FD_ZERO 将客户端socket集合清零 FD_ZERO(&clientSockets);
FD_SET 添加一个socket(超过默认值大小不再处理) FD_SET(socketListen, &clientSockets);
FD_CLR 从集合中删除指定的socket一定要close,手动释放 FD_CLR(socketListen,&clientSockets);
closesocket(socketListen);
FD_ISSET 查询socket是否在集合中,不存在返回0,存在返回非0 FD_ISSET(socketListen, &clientSockets);

getsockopt函数

1
2
3
4
5
6
7
int getsockopt(
SOCKET s;//套接字
int level;//选顶的等级包括SOL_SOCKET IPPROTO_TCP
int optname;//SOCKET选顶的名称SO_ERROR SO_ACCEPTCONN
char FAR *optval;//用于接收的缓冲区
int FAR *optlen //大小
);

成功返回0;失败返回SOCKET_ERROR

代码

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
int main(int argc, char* argv[])
{
// 0. 初始化
WORD wsVersion = MAKEWORD(2, 2);
WSADATA wsaData = {0};
WSAStartup(wsVersion, &wsaData);
// 1. 创建套接字
SOCKET sSocket = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKET_ERROR == sSocket) {
printf("套接字闯创建失败!\n" );
}
else {
printf("套接字闯创建成功!\n" );
}
// 2. 绑定套接字
sockaddr_in sockAddrInfo = {0}; // 初始化
sockAddrInfo.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
sockAddrInfo.sin_port = htons(2118); // 端口
sockAddrInfo.sin_family = AF_INET; // 地址族规范

int bRes = bind(sSocket, (sockaddr*)&sockAddrInfo, sizeof(sockAddrInfo));

if (SOCKET_ERROR == bRes) {
printf("绑定失败!\n");
}
else {
printf("绑定成功!\n");
}
// 3. 监听套接字
int lRes = listen(sSocket, 5);
if (SOCKET_ERROR == lRes) {
printf("监听失败!\n");
}
else {
printf("监听成功!\n");
}

fd_set fdSocket; // 定义
FD_ZERO(&fdSocket); // 初始化
FD_SET(sSocket, &fdSocket); // 将当前服务器创建的socket放入集合中

while (true) {
fd_set readfds = fdSocket; // 定义可读的集合
fd_set writefds = fdSocket; // 定义可写的集合
// fd_set exceptfds = fdSocket;

// 获取select函数的返回值
int iRes = select(0, &readfds, &writefds, NULL, NULL);
// 如果返回值大于0则说明不存在无响应、错误的情况,继续向下
if (iRes > 0) {
// 遍历可写集合,给每个socket发送Hello
for (u_int i = 0; i < writefds.fd_count; i++) {
send(readfds.fd_array[i], "Hello", 6, 0);
}

// 遍历可读集合
for (i = 0; i < readfds.fd_count; i++) {
// 如果socket为当前服务器创建的scoket则进入accept等待消息
if (readfds.fd_array[i] == sSocket) {
sockaddr_in s = {0};
int l = sizeof(s);
SOCKET aSocket = accept(sSocket, (sockaddr*)&s, &l);
if (INVALID_SOCKET == aSocket)
{
continue;
}
FD_SET(aSocket, &fdSocket);
// inet_ntoa获取IP
printf("Accpet Client IP: %s \n", inet_ntoa(s.sin_addr));
// 如果不是,则进入接收消息
} else {
char buf[100] = {0};
int iRecv = recv(readfds.fd_array[i], buf, sizeof(buf), 0);
// 判断接收消息的返回值,大于0则表示接收成功。
if (iRecv > 0) {
printf("Recv: %s \n", buf);
// 否则就关闭连接、关闭套接字
} else {
SOCKET tSocket = readfds.fd_array[i];
FD_CLR(tSocket, &fdSocket);
shutdown(tSocket, SD_BOTH);
closesocket(tSocket);
}
}
}
} else {
continue;
}
}

closesocket(sSocket);

WSACleanup();
return 0;
}

WSAAsyncSelect模型

WSAAsyncSelect模式允许以windows消息的形式接收网络事件通知。这个模式是为了适应windows消息驱动环境而设置的,对性能要求不高的网络应用程序可采用此模式。

优缺点:WSAAsyncSelect模型最突出的特点是与windows的消息驱动机制融在了一起,这使得开发带GUI界面的网络程序变得很简单。但是如果连接增加,单个WINDOWS函数处理上千个客户请求时,服务器性能势必发受到影响。

WSAAsyncSelect()函数自动把套接字设为非阻塞模式,并且为套接字绑定一个窗口句柄,当有网络事件发生时,便向这个窗口发送消息。

语法:

1
2
3
4
5
6
int WSAAPI WSAAsyncSelect(
[in] SOCKET s, //标识需要事件通知的套接字的描述符
[in] HWND hWnd, //标识在发生网络事件时接收消息的窗口的句柄
[in] u_int wMsg, //发生网络事件时要接收的消息
[in] long lEvent //位掩码,指定应用程序感兴趣的网络事件的组合
);

lEvent参数:

含义
FD_READ 欲接收 读准备好 的通知.
FD_WRITE 欲接收 写准备好 的通知.
FD_OOB 欲接收 带边数据到达 的通知.
FD_ACCEPT 欲接收 将要连接 的通知.
FD_CONNECT 欲接收 已连接好 的通知.
FD_CLOSE 欲接收 套接口关闭 的通知.

服务端示例

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#include <windows.h>
#include <winsock2.h>
#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

#define PORT 8080 // 定义服务器监听的端口号
#define WM_SOCKET WM_USER + 1 // 定义自定义消息,用于处理socket事件

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // 声明窗口过程函数

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
WNDCLASSEX wcex;
HWND hwnd;
MSG msg;
SOCKET server_fd;
struct sockaddr_in address;

// 初始化 Winsock
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}

// 注册窗口类
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = "WSAAsyncSelectExample";
wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

if (!RegisterClassEx(&wcex)) {
printf("RegisterClassEx failed: %d\n", GetLastError());
return 1;
}

// 创建窗口
hwnd = CreateWindow("WSAAsyncSelectExample", "WSAAsyncSelect Example", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

if (!hwnd) {
printf("CreateWindow failed: %d\n", GetLastError());
return 1;
}

ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);

// 创建socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
printf("socket failed: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}

// 绑定socket
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) == SOCKET_ERROR) {
printf("bind failed: %d\n", WSAGetLastError());
closesocket(server_fd);
WSACleanup();
return 1;
}

// 监听
if (listen(server_fd, 3) == SOCKET_ERROR) {
printf("listen failed: %d\n", WSAGetLastError());
closesocket(server_fd);
WSACleanup();
return 1;
}

// 设置WSAAsyncSelect
if (WSAAsyncSelect(server_fd, hwnd, WM_SOCKET, FD_ACCEPT | FD_READ | FD_CLOSE) == SOCKET_ERROR) {
printf("WSAAsyncSelect failed: %d\n", WSAGetLastError());
closesocket(server_fd);
WSACleanup();
return 1;
}

// 消息循环
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

closesocket(server_fd);
WSACleanup();

return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
SOCKET client_fd;
char buffer[1024];
int iResult;

switch (msg) {
case WM_SOCKET:
if (WSAGETSELECTERROR(lParam)) {
printf("Socket error: %d\n", WSAGETSELECTERROR(lParam));
closesocket((SOCKET)wParam);
break;
}

switch (WSAGETSELECTEVENT(lParam)) {
case FD_ACCEPT:
// 接受新连接
client_fd = accept((SOCKET)wParam, NULL, NULL);
if (client_fd == INVALID_SOCKET) {
printf("accept failed: %d\n", WSAGetLastError());
} else {
// 为新连接的socket设置WSAAsyncSelect
if (WSAAsyncSelect(client_fd, hwnd, WM_SOCKET, FD_READ | FD_CLOSE) == SOCKET_ERROR) {
printf("WSAAsyncSelect failed: %d\n", WSAGetLastError());
closesocket(client_fd);
}
}
break;

case FD_READ:
// 读取数据
iResult = recv((SOCKET)wParam, buffer, sizeof(buffer), 0);
if (iResult > 0) {
buffer[iResult] = '\0';
printf("Received: %s\n", buffer);
send((SOCKET)wParam, buffer, iResult, 0);
} else if (iResult == 0) {
printf("Connection closed\n");
closesocket((SOCKET)wParam);
} else {
printf("recv failed: %d\n", WSAGetLastError());
closesocket((SOCKET)wParam);
}
break;

case FD_CLOSE:
// 连接关闭
printf("Connection closed\n");
closesocket((SOCKET)wParam);
break;
}
break;

case WM_DESTROY:
// 窗口关闭消息
PostQuitMessage(0);
break;

default:
return DefWindowProc(hwnd, msg, wParam, lParam);
}

return 0;
}