抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

基于select模型的TCP服务器

实验工程文件下载

https://gryffinbit.lanzous.com/iSR7Gjhg68b

实验目的

  1. 熟悉非阻塞套接字的工作模式
  2. 理解select模型的基本思路

实验环境

Windows10 x64,Visual Studio 2017

实验要求

将第三章的点对点的C/S模型改用select模型。为了方便理解,我们只是关注可读套接字,也就是select函数的第三个和第四个参数均为NULL。

实验原理(模型)

select的函数

1
2
3
4
5
6
7
int select(
int nfds,
fd_set FAR* readfds,
fd_set FAR* writefds,
fd_set FAR* exceptfds,
const struct timeval FAR* timeout
);
  • fd_set数据类型,代表着一系列特定套接字的集合。
  • nfds : 为保持与早期套接字应用程序兼容。
  • readfds: 用于检查可读性。该集合包括想要检查是否符合下述任何一个条件的套接字。
    • 有数据到达,可以读入
    • 连接已关闭、重设或中止
    • 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。
  • wirtefds:用于检查可写性。该集合包括想要检查是否符合下述任何一个条件的套接字。
    • 发送缓冲区已空,可以发送数据。
    • 如果已完成了对一个非锁定连接调用的处理,连接就会成功。
  • exceptfds:用于检查带外数据。该集合包括想要检查是否符合下述任何一个条件的套接字。
    • 假如已完成了对一个非锁定连接调用的处理,连接尝试就会失败。
    • 有带外(out-of-band,OOB)数据可供读取
  • timeout:是一个指向一个timeval结构的指针,用于决定select等待I/O操作完成的最长时间。如果timeout是一个空指针,那么select调用会无限制地等待下去,直到至少有一个套接字符合制定的条件后才结束。

timeval结构的定义

1
2
3
4
strcut timeval{
long tv_sec; //以秒为单位指定等待时间
long tv_usec; //以毫秒为单位指定等待时间
};

select模型的操作步骤

用select操作一个或多个套接字句柄,一般采用下述步骤。

  1. 使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set集合。
  2. 使用FD_SET宏,将要检查的套接字句柄添加到自己感兴趣的每哥fd_set集合中,相当于在指定的fd_set集合中,设置好要检查的I/O活动。
  3. 调用select函数,然后等待。select完成返回后,会修改每个fd_set结构,删除那些不存在待决I/O操作的套接字句柄,在各个fd_set集合中返回符合条件的套接字。
  4. 根据select的返回值,使用FD_ISSET宏,对每个fd_set集合进行检查,判断一个待定的套接字是否仍在集合中,便可判断出哪些套接字存在着尚未完成的I/O操作。
  5. 知道了每个集合中未完成的I/O操作之后,对相应的套接字的I/O进行处理,然后返回步骤1,继续进行select处理。

实验过程

  1. 将客户端中调用recv函数的部分都删掉,也就是客户端只能给服务器发消息,不能接收消息。

  2. select模型也需要引用头文件库文件,打开网络库,校验版本,创建监听套接字,绑定地址,开始监听,然后创建一个fd_set结构体,并把监听套接字放这个集合中。

  3. 在一个死循环中调用select函数,并判断select的返回值。

    若返回值为SOCKET_ERROR,则报错并做相应处理;

    若返回值为0, 则继续;

    若返回值大于0,说明select函数有反应。有反应就要区分是监听套接字还是响应套接字导致的。若是监听套接字导致的,就调用accept函数,创建一个新的响应套接字,并把这个响应套接字放到第2步定义的fd_set结构当中。如果是响应套接字,就调用recv函数接收客户机发来的消息。

  4. 最后释放掉所有的套接字,并关闭网络库

  5. 测试。先开始运行select模型,然后在客户端的Debug/Release文件夹中双击exe文件即可打开多个客户端,这些客户端均可给服务器发消息。

  6. 测试完成后,可以在accept函数之后调用send函数,发消息给客户端提示连接成功。相应的,就需要先在客户端调用recv函数接收此消息。

👉创建服务器端

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
168
169
170
171
172
173
174
#include <stdio.h>        
#include <stdlib.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
//winsock2.h转到文档,可以查看当且编译器环境支持的最高版本

//引用库文件
#pragma comment(lib,"Ws2_32.lib")

int main(void)
{
//打开网络库
WORD wdVersion = MAKEWORD(2, 2);
WSADATA wsaData;

if (0 != WSAStartup(wdVersion, &wsaData))
{
printf("WSAStartup fail!");
return -1;
}

//校验版本
if (2 != HIBYTE(wsaData.wVersion) || 2 != LOBYTE(wsaData.wVersion))
{
printf("Version fail!");
//关闭库
WSACleanup();
return -1;
}

//创建监听套接字
SOCKET socketListen = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKET_ERROR == socketListen)
{
printf("socket fail!");
//关闭库
WSACleanup();
return -1;
}

//绑定地址
SOCKADDR_IN sockAddress;
sockAddress.sin_family = AF_INET;//通信域为IPV4
//sockAddress.sin_addr.s_addr = inet_addr("127.0.0.1");//回送地址,主要用于网络软件测试以及本地机进程间通信
inet_pton(AF_INET, "127.0.0.1", (void*)&sockAddress.sin_addr);
sockAddress.sin_port = htons(51314);

if (SOCKET_ERROR == bind(socketListen, (struct sockaddr*)&sockAddress, sizeof(sockAddress)))
{
printf("bind fail!");
//关闭监听套接字
closesocket(socketListen);
//关闭库
WSACleanup();
return -1;
}

//开始监听
if (SOCKET_ERROR == listen(socketListen, 5))
{
printf("listen fail!");
//关闭监听套接字
closesocket(socketListen);
//关闭库
WSACleanup();
return -1;
}

//申明一个fd_set结构体存放套接字,可以右键转定义查看它的结构
fd_set allSockets;
//初始化
FD_ZERO(&allSockets);
//把监听套接字放到fd_set这个集合中
FD_SET(socketListen, &allSockets);

while (true)
{
//由于给select传入一个fd_set结构体,它只返回fd_set中有响应的套接字,所以需要定义一个中转变量
fd_set TempSockets = allSockets;

//最大等待时间
struct timeval st;
st.tv_sec = 10;
st.tv_usec = 0;

//调用select函数,这里我们将第三个和第四个参数设为了NULL,
//即只是关心可读的套接字,也就是调用accept和recv的情况
int ret = select(0, &TempSockets, NULL, NULL, &st);
//判断select的返回值分为SOCKET_ERROR,0, 和大于0三种情况
if (ret == SOCKET_ERROR)
{
printf("select fail!");
break;
}

if (0 == ret) //没有反应,继续
{
continue;
}

if (ret > 0)
{
//select函数有反应,有反应就要区分是监听套接字还是响应套接字导致的
for (u_int i = 0; i < TempSockets.fd_count; i++)
{
//是监听套接字导致的
if (TempSockets.fd_array[i] == socketListen)
{
//需要创建响应套接字
SOCKET SockAccept = accept(socketListen, NULL, NULL);
//判断accept函数是否调用失败,失败的返回值是INVALID_SOCKET,不是SOCKET_ERROR
if (SockAccept == INVALID_SOCKET)
{
//创建响应套接字失败
printf("创建响应套接字失败!");
continue;
}
//把新创建的响应套接字也放到fd_set集合中
FD_SET(SockAccept, &allSockets);
//给客户端发消息
if (SOCKET_ERROR == send(SockAccept, "我是服务器,您已成功连接", strlen("我是服务器,您已成功连接"), 0))
{
printf("send fail!");
//关闭响应套接字
closesocket(SockAccept);
//关闭库
WSACleanup();
return -1;
}
}
else
{
//是响应套接字导致的,那就需要调用recv函数
//判断客户端连接的集合中是否有需要接收的数据
char szRecvBuffer[1024] = { 0 };

int nReturnValue = recv(TempSockets.fd_array[i], szRecvBuffer, sizeof(szRecvBuffer) - 1, 0);

if (0 == nReturnValue)
{
//客户端正常关闭
printf("客户端已下线");
//从集合中删掉Socket,还得调用closesocket关闭这个socket
SOCKET SockTemp = TempSockets.fd_array[i];
FD_CLR(TempSockets.fd_array[i], &allSockets);
closesocket(SockTemp);
}
else if (SOCKET_ERROR == nReturnValue)
{
//网络中断
int nRes = WSAGetLastError();
printf("响应套接字%d所对应的客户端中断连接\n", TempSockets.fd_array[i]);
//同样需要从集合删掉套接字和关闭它。
}
else
{
//接收到客户端消息
printf("Data from Socket %d : %s \n", TempSockets.fd_array[i],szRecvBuffer);
}
}
}
}
}
//释放所有socket
for (u_int i = 0; i < allSockets.fd_count; i++)
{
closesocket(allSockets.fd_array[i]);
}
//关闭网络库
WSACleanup();

system("pause");
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
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#include <WS2tcpip.h>

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

int main()
{
//打开网络库
WORD wdVersion = MAKEWORD(2, 2);
WSADATA wsaData;

if (0 != WSAStartup(wdVersion, &wsaData))
{

printf("WSAStartup fail!");
return -1;
}

//校验版本
if (2 != HIBYTE(wsaData.wHighVersion) || 2 != LOBYTE(wsaData.wVersion))
{
printf("Version fail!");
//关闭库
WSACleanup();
return -1;
}

//创建一个SOCKET 监听
//套接字的类型,使用的协议一定要和服务器的类型和协议匹配上
SOCKET socketUser = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKET_ERROR == socketUser)
{
printf("socket fail!");
//关闭库
WSACleanup();
return -1;
}

//初始化服务器端的地址,IP和端口一定要和想要连接的服务器一样,不然就连接不上
SOCKADDR_IN sockAddress;
sockAddress.sin_family = AF_INET; //IPv4的协议
//sockAddress.sin_addr.s_addr = inet_addr("127.0.0.1");
inet_pton(AF_INET, "127.0.0.1", (void*)&sockAddress.sin_addr); //服务器地址
sockAddress.sin_port = htons(51314); //服务器的端口

//客户机端主动连接服务器端
if (SOCKET_ERROR == connect(socketUser, (struct sockaddr*)&sockAddress, sizeof(sockAddress)))
{

printf("connect fail!\n");
//int a=WSAGetLastError();
//printf("error code : %d\n", a);
//关闭套接字
closesocket(socketUser);
//关闭库
WSACleanup();
return -1;
}

//接收服务器返回的信息
char szBuffer[1024] = { 0 };
//接收数据, 跟服务器端一样
int nReturnValue = recv(socketUser, szBuffer, sizeof(szBuffer), 0);

if (0 == nReturnValue)
{
//客户端正常关闭 释放Socket
closesocket(socketUser);
}
else if (SOCKET_ERROR == nReturnValue)
{
//网络中断
printf("连接中断");
}
else
{
//接收到客户端消息
printf("server data : %s\n", szBuffer);



//发送消息给服务器
while (1)
{

//发送数据, 跟服务器端一样
char szSendData[1024] = { 0 };
printf("Input Something : ");
scanf_s("%s", szSendData, 1024);
if (SOCKET_ERROR == send(socketUser, szSendData, strlen(szSendData), 0))
{

printf("send fail!");
//关闭客户机Socket
closesocket(socketUser);
//关闭库
WSACleanup();
return -1;
}
}
}

//关闭socket
closesocket(socketUser);
//关闭网络库
WSACleanup();

system("pause");
return 0;
}

👉最后运行客户端,生成解决方案之后,到工程文件夹里找到exe,开启多个客户端。

评论