【c++项目】从零开始写一个局域网聊天室05:客户端升级为select模型

本文最后更新于:2022年12月31日 下午

【c++项目】从零开始写一个局域网聊天室05:客户端升级为select模型

详细的过程以及讲解我都以注释的形式写在代码里,这里就不啰嗦了。

server端

server.cpp

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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
/*
author: 连思鑫
2022-09-08
客户端升级为select模型

*/

#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <windows.h>
#include <WinSock2.h>
#include "iostream"
#include "stdio.h"
#include <vector>
using namespace std;
/*使用报文的方式进行传输*/
//数据头
enum CMD
{
CMD_LOGIN,
CMD_LOGIN_RESULT,
CMD_LOGINOUT,
CMD_LOGINOUT_RESULT,
CMD_NEW_USER_JOIN,
CMD_ERROR
};
struct DataHeader
{
short dataLength;//数据长度
short cmd;//命令
};
//DataPackage
//包体
struct Login : public DataHeader
{
//DataHeader header;
Login()
{
dataLength = sizeof(Login);
cmd = CMD_LOGIN;
}
char userName[32];
char PassWord[32];
};
struct LoginResult : public DataHeader
{
LoginResult()
{
dataLength = sizeof(LoginResult);
cmd = CMD_LOGIN_RESULT;
result = 0;
}
int result;
};
struct LoginOut : public DataHeader
{
LoginOut()
{
dataLength = sizeof(LoginOut);
cmd = CMD_LOGINOUT;
}
char userName[32];
};
struct LoginOutResult : public DataHeader
{
LoginOutResult()
{
dataLength = sizeof(LoginOutResult);
cmd = CMD_LOGINOUT_RESULT;
result = 0;
}
int result;
};
//新客户端加入
struct NewUserJoin : public DataHeader
{
NewUserJoin()
{
dataLength = sizeof(NewUserJoin);
cmd = CMD_NEW_USER_JOIN;
sock = 0;
}
int sock;
};
//创建一个全局的容器
vector<SOCKET> g_clients;

//创建一个函数进行处理
int processor(SOCKET _cSocket)
{
//使用缓冲区来接受数据
char szRecv[1024] = {};
//5 接收客户端请求数据
int nLen = recv(_cSocket, (char*)&szRecv, sizeof(DataHeader), 0);
//拆包 和 分包
/*拆包和分包的作用主要是用在服务端接受数据时一次接受数据过长 和 过短的情况*/
DataHeader* header = (DataHeader*)szRecv;
if (nLen <= 0)
{
printf("客户端<Socket = %d>已经退出, 任务结束。\n", _cSocket);
return -1;
}
//printf("收到命令: %d 数据长度:%d\n", header.cmd, header.dataLength);
/*判断所收到的数据*/ //多客户端进行收发数据的情况下使用
//if (nLen > sizeof(DataHeader))
//{
//}
switch (header->cmd)
{
case CMD_LOGIN:
{
recv(_cSocket, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
Login* login = (Login*)szRecv;
printf("收到客户端<Socket = %d>请求:CMD_LOGIN 数据长度:%d userName = %s passWord = %s\n",_cSocket, login->dataLength, login->userName, login->PassWord);
//忽略判断用户名密码是否正确的过程
LoginResult ret;
//send(_cSocket, (char*)&header, sizeof(DataHeader), 0);
send(_cSocket, (char*)&ret, sizeof(LoginResult), 0);
}
break;
case CMD_LOGINOUT:
{
recv(_cSocket, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
LoginOut *loginout = (LoginOut*)szRecv;
printf("收到命令:CMD_LOGINOUT 数据长度:%d userName = %s\n", loginout->dataLength, loginout->userName);
//忽略判断用户名密码是否正确的过程
LoginOutResult ret;
//send(_cSocket, (char*)&header, sizeof(DataHeader), 0);
send(_cSocket, (char*)&ret, sizeof(LoginOutResult), 0);
}
break;
default:
{
DataHeader header = { 0, CMD_ERROR };
send(_cSocket, (char*)&header, sizeof(DataHeader), 0);
}
break;
}
}

int main()
{
/*启动Windows socket 2.x环境*/
//版本号
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
//socket网络编程启动函数
WSAStartup(ver, &dat);
//---------------------------
//--用Socket API建立简易TCP服务端
//1、建立一个socket 套接字 (windows) linux上指的是指针
/*socket(
_In_ int af,(表示什么类型的套接字)
_In_ int type,(数据流)
_In_ int protocol
);*/
//IPV4的网络套接字 AF_INET
//IPV6的网络套接字 AF_INET6
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
char msgBuf[] = "Hello, I'm Server.\n";
//2、bind 绑定用于接收客户端链接的网络端口
/*
bind(
_In_ SOCKET s,
_In_reads_bytes_(namelen) const struct sockaddr FAR * name,
_In_ int namelen
);
*/
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;//ipv4
_sin.sin_port = htons(4567);//端口号 由于主机和网络的数据类型不同 因此需要进行转换 使用 host to net unsigned short
_sin.sin_addr.S_un.S_addr = INADDR_ANY;//inet_addr("127.0.0.1");//服务器的ip地址 INADDR_ANY本机所有的Ip地址都可以访问 一般这样
//有可能绑定失败
if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))
{
printf("错误,绑定网络端口失败...\n");
}
else
{
printf("绑定端口成功...\n");
}

//3、listen 监听网络端口
/*
listen(
_In_ SOCKET s,
_In_ int backlog
);*/
if (SOCKET_ERROR == listen(_sock, 5))
{
printf("错误,监听网络端口失败...\n");
}
else
{
printf("监听网络端口成功...\n");
}
while (true)
{
// 伯克利 BSD socket windows上第一个参数无意义
//linux 表示描述符加1
/*处理多客户端类型
select(
_In_ int nfds,
_Inout_opt_ fd_set FAR * readfds,//可读
_Inout_opt_ fd_set FAR * writefds,//可写
_Inout_opt_ fd_set FAR * exceptfds,//异常
_In_opt_ const struct timeval FAR * timeout//查询延迟
);*/
fd_set fdRead;//描述符(socket)集合
fd_set fdWrite;
fd_set fdExp;

FD_ZERO(&fdRead);//清空fd_set集合类型的数据 其实就是将fd_count 置为0
FD_ZERO(&fdWrite);//清理集合
FD_ZERO(&fdExp);
//typedef struct fd_set {
// u_int fd_count; 数量 /* how many are SET? */
// SOCKET fd_array[FD_SETSIZE(64)]; /* an array of SOCKETs */
//} fd_set;

FD_SET(_sock, &fdRead);//将描述符加入集合中
FD_SET(_sock, &fdWrite);
FD_SET(_sock, &fdExp);
for (int n = (int)g_clients.size() - 1; n >= 0; n --)
{
FD_SET(g_clients[n], &fdRead);//放入可读数据中查询 是否有可读数据
}
//nfds是一个整数值 是指fd_set集合中所有描述符(socket)的范围 而不是数量
//既是所有文件描述符最大值+1 在windows中这个参数可以写0
//最后一个参数写成NULL表示一直阻塞在此等待
timeval t = {1, 0};//时间变量 &t 最大为1秒
//struct timeval {
//long tv_sec; /* seconds */
//long tv_usec; /* and microseconds */};
int ret = select(_sock + 1, &fdRead, &fdWrite, &fdExp, &t);
/*以上方式为阻塞方式,如果没有客户端进入将阻塞在此处*/
if (ret < 0)
{
printf("select任务结束。\n");
break;//表示出错 跳出循环
}
//如果这个socket可读的话表示 有客户端已经连接进来了
if (FD_ISSET(_sock, &fdRead))//判断描述符是否在集合中
{
//清理
FD_CLR(_sock, &fdRead);
//4、accept 等待接收客户端链接
/*
accept(
_In_ SOCKET s,
_Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr,
_Inout_opt_ int FAR * addrlen
);*/
//accept 等待接受客户端连接
sockaddr_in clientAddr = {};
int nAddrLen = sizeof(sockaddr_in);
SOCKET _cSocket = INVALID_SOCKET;
_cSocket = accept(_sock, (sockaddr*)&clientAddr, &nAddrLen);
if (INVALID_SOCKET == _cSocket)
{
printf("错误,接收到无效客户端SOCKET...\n");
}
else
{
//有新客户端加入群发给所有用户 类似像聊天室 或者狼人杀类型
for (int n = (int)g_clients.size() - 1; n >= 0; n--)
{
NewUserJoin userJoin;
//发送给现有的每个客户端
send(g_clients[n], (const char*)&userJoin, sizeof(NewUserJoin), 0);
}
g_clients.push_back(_cSocket);
printf("新客户端加入: socket = %d IP = %s \n", _cSocket, inet_ntoa(clientAddr.sin_addr));//inet_ntoa转换为可读地址
}

}
for (size_t n = 0; n < fdRead.fd_count; n++)
{
if (processor(fdRead.fd_array[n]) == -1)
{
auto iter = find(g_clients.begin(), g_clients.end(), fdRead.fd_array[n]);
if (iter != g_clients.end())
{
g_clients.erase(iter);
}
}
}
printf("空闲时间处理其他业务\n");
}
//以防万一退出程序时 将所有套接字进行清理
for (size_t n = g_clients.size() - 1; n > 0; n --)
{
closesocket(g_clients[n]);
}
//5、send 向客户端发送一条数据
/*send(
_In_ SOCKET s,
_In_reads_bytes_(len) const char FAR * buf,
_In_ int len,
_In_ int flags
);*/
//char msgBuf[] = "Hello, I'm Server.";
//+1表示将结尾符一并发送过去
//send(_cSocket, msgBuf, strlen(msgBuf) + 1, 0);

//6、关闭套接字closesocket
closesocket(_sock);

//---------------------------
WSACleanup();
printf("已退出,任务结束\n");
getchar();
return 0;
}

client端

client.cpp

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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/*
author: 连思鑫
2022-09-08
客户端升级为select模型

*/
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <windows.h>
#include <WinSock2.h>
#include <stdio.h>
#include "iostream"
using namespace std;
enum CMD
{
CMD_LOGIN,
CMD_LOGIN_RESULT,
CMD_LOGINOUT,
CMD_LOGINOUT_RESULT,
CMD_NEW_USER_JOIN,
CMD_ERROR
};
struct DataHeader
{
short dataLength;//数据长度
short cmd;//命令
};
//DataPackage
//包体
struct Login : public DataHeader
{
//DataHeader header;
Login()
{
dataLength = sizeof(Login);
cmd = CMD_LOGIN;
}
char userName[32];
char PassWord[32];
};
struct LoginResult : public DataHeader
{
LoginResult()
{
dataLength = sizeof(LoginResult);
cmd = CMD_LOGIN_RESULT;
result = 0;
}
int result;
};
struct LoginOut : public DataHeader
{
LoginOut()
{
dataLength = sizeof(LoginOut);
cmd = CMD_LOGINOUT;
}
char userName[32];
};
struct LoginOutResult : public DataHeader
{
LoginOutResult()
{
dataLength = sizeof(LoginOutResult);
cmd = CMD_LOGINOUT_RESULT;
result = 0;
}
int result;
};
//新客户端加入
struct NewUserJoin : public DataHeader
{
NewUserJoin()
{
dataLength = sizeof(NewUserJoin);
cmd = CMD_NEW_USER_JOIN;
sock = 0;
}
int sock;
};
int processor(SOCKET _cSocket)
{
//使用缓冲区来接受数据
char szRecv[1024] = {};
//5 接收客户端请求数据
int nLen = recv(_cSocket, (char*)&szRecv, sizeof(DataHeader), 0);
//拆包 和 分包
/*拆包和分包的作用主要是用在服务端接受数据时一次接受数据过长 和 过短的情况*/
DataHeader* header = (DataHeader*)szRecv;
if (nLen <= 0)
{
printf("与服务器断开连接, 任务结束。\n", _cSocket);
return -1;
}
//printf("收到命令: %d 数据长度:%d\n", header.cmd, header.dataLength);
/*判断所收到的数据*/ //多客户端进行收发数据的情况下使用
//if (nLen > sizeof(DataHeader))
//{
//}
switch (header->cmd)
{
case CMD_LOGIN_RESULT:
{
recv(_cSocket, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
LoginResult* login = (LoginResult*)szRecv;
printf("收到服务端消息:CMD_LOGIN_RESULT 数据长度:%d \n", _cSocket, login->dataLength);
}
break;
case CMD_LOGINOUT_RESULT:
{
recv(_cSocket, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
LoginOutResult* loginout = (LoginOutResult*)szRecv;
printf("收到服务端消息:CMD_LOGINOUT_RESULT 数据长度:%d \n", _cSocket, loginout->dataLength);
}
break;
case CMD_NEW_USER_JOIN:
{
recv(_cSocket, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
NewUserJoin* userJoin = (NewUserJoin*)szRecv;
printf("收到服务端消息:CMD_NEW_USER_JOIN 数据长度:%d \n", _cSocket, userJoin->dataLength);
}
break;
}
}
int main()
{
/*启动Windows socket 2.x环境*/
//版本号
WORD ver = MAKEWORD(2, 2);
WSADATA dat;
//socket网络编程启动函数
WSAStartup(ver, &dat);
//---------------------------
//用Socket API建立简易TCP客户端
//1、建立一个socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == _sock)
{
printf("错误,建立Socket失败...\n");
}
else
{
printf("建立socket成功...\n");
}

//2、链接服务器 connect
sockaddr_in _sin = {};//将结构体初始化
_sin.sin_family = AF_INET;//ipv4
_sin.sin_port = htons(4567);//将网络转换为成无符号类型
_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in)); //使用sizeof(sockaddr_in)类型更安全
if (SOCKET_ERROR == ret)
{
printf("错误,链接服务器失败...\n");
}
else
{
printf("链接服务器成功...\n");
}
while (true)
{
fd_set fdReads;
FD_ZERO(&fdReads);
FD_SET(_sock, &fdReads);
timeval t = { 0, 0 };
int ret = select(_sock, &fdReads, 0, 0, &t);
if (ret < 0)
{
printf("select任务结束!!!\n");
break;
}
if (FD_ISSET(_sock, &fdReads))
{
FD_CLR(_sock, &fdReads);
if (processor(_sock) == -1)
{
printf("select任务结束2\n");
break;
}

}
printf("空闲时间处理其他业务\n");
Login login;
strcpy(login.userName, "zjj");
strcpy(login.PassWord, "456451");
send(_sock, (const char*)&login, sizeof(login), 0);
Sleep(1000);
}

//7 关闭套节字closesocket
closesocket(_sock);
//---------------------------
WSACleanup();
printf("已退出.\n");
getchar();
return 0;
}

成果展示

upload successful

server.exe体验:点我下载

client.exe体验:点我下载


【c++项目】从零开始写一个局域网聊天室05:客户端升级为select模型
https://jinbilianshao.github.io/2022/09/08/【c-项目】从零开始写一个局域网聊天室05:客户端升级为select模型/
作者
连思鑫
发布于
2022年9月8日
许可协议