0%

「orange」

前言

这应该是我自己从头开始自己思考、自己设计、自己写代码的第一个项目了。

想做个小聊天软件,以命令行为主,我喜欢这种风格。不过因为是命令行交互,为了支持更多功能,所以整个客户端程序就变成了一个巨大的状态机,设计起来有些困难。

难点在于要单缓冲区多线程,一个输出输入缓冲(黑框框)要键入命令、接收随时可能到来的输出;并且用户可能输入一些不对应状态的命令,这也需要处理;最后还必须处理网络时延的问题。

项目代码链接:Chen-Jin-yuan/orange (github.com)

设计分析

  • 单reactor多线程模式
  • 主要有三种功能
    • 命令:客户端向服务器发送命令进行交互,命令行状态下直接输入,在其他状态下使用@字符:@cmd
    • 聊天:客户之间能进行文字聊天通信,数据通信路径为客户->服务器->客户,这样服务器可以记录聊天数据
      • 需要注册登录,有一个唯一的登录id,链接到数据库;
      • 登录后可以设置**标识字符串sid(search id)**,以及标识用户名sname。sid用于其他客户寻找本用户,即建立一个临时的会话(好玩的地方),sid在同一时间不能相同;标识用户名可任意,通信时显示。
    • 文件传输:进行p2p的文件传输
      • 用户A可以通过命令获取用户B的文件资源表(resource文件夹下放置可传输的文件),经用户B同意后可以传输
      • 用户A也可以主动发送文件给用户B,用户B同意后可接收
  • 调用主要以命令为主,本质是一台状态机,难点在于用户输入只有一个缓冲,状态不能交叉影响

命令系统

这里设计命令系统,用户在普通情况下是一个命令行输入,即cmd> ,这里为了清楚标识,命令关键字以**@**标识。

命令 作用
@register [userID] [password] [password again] 注册一个账号,用户名和密码不能含空格,用户名不能重
@login [userID] [password] 登录账号
@search [sid] 搜索某个sid,若存在则返回对方sname和是否在chat的信息;对方不会知道有用户搜索ta
@chat [sid] 准备进入chat状态,描述较长见表格后
@accept [sid] 接受某个sid,收到chat请求后会维护更新本地客户端的一个请求表,需要存在该sid
@reject [sid] 拒绝某个sid,收到chat请求后会维护更新本地客户端的一个请求表,需要存在该sid
@break 退出状态,进入命令行状态,描述较长见表格后
@send [sid] [msg] 直接向某个sid(若存在)发送一条信息,对方会得知sid和sname
@sendfile [sid] [path] 直接向某个sid(若存在)发送一条文件发送信息,对方会得知sid和sname和文件名,进入等待
@acceptfile [sid] 接收某sid发送的文件,返回接收信息
@rejectfile [sid] 拒绝某sid发送的文件,返回拒绝信息
@getfile [sid] 请求获取对方的文件资源表,对方会得知sid和sname,进入等待
@acceptget [sid] 同意对方获取资源,返回接受信息,等待对方选择文件
@rejectget [sid] 拒绝对方获取资源,返回拒绝信息
@setsid [newsid] 设置sid,不设置默认用登录的userID
@setsname [newsname] 设置sname,不设置默认用登录的userID
@re 重复上一条命令
@exit 退出
@hisir [msg] 向服务器说点事情,会得到服务器返回的一句话
@oyasumi 退出,向服务器说晚安
  • @chat:直接与某个sid用户通信,如果没有该sid会返回错误信息1;否则对方会接收到请求,获得你的sid和sname,并选择是否接受。若拒绝则返回拒绝信息;若接受则进入chat状态。用户在调用chat命令后,会进入等待状态(等待对方接受)
  • @break:
    • 退出等待状态,这会告知对方已经不请求chat了,同时删除对方的请求表中的sid,返回命令行状态
    • 退出chat状态,这会告知对方已经断开chat了,返回命令行状态
    • 退出发送文件等待状态,和chat一样
    • 退出获取文件资源表的等待状态
  • @sendfile:调用break可以退出等待;若对方接收,会开辟一个发送文件线程;若对方拒绝,直接退出等待
  • @getfile:若对方同意,则可以开始选择文件

客户端

我们先随意写一些功能函数,最后封装起来,一些全局变量最后会变成类成员变量,请不用担心。

命令解析

首先解析命令,从命令行读入字符串然后分析。命令解析在本地完成,不发送给服务器,这样能降低出错的可能并降低服务器的复杂度。

下面是一个解析命令的demo

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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
#include<iostream>
#include<cstring>//string.h
#include<string>
#include<unordered_map>
using namespace std;

const unordered_map<string, int> cmdmap =
{
{"register",0},
{"login",1},
{"search",2},
{"chat",3},
{"accept",4},
{"reject",5},
{"break",6},
{"send",7},
{"sendfile",8},
{"acceptfile",9},
{"rejectfile",10},
{"getfile",11},
{"acceptget",12},
{"rejectget",13},
{"setsid",14},
{"setsname",15},
{"re",16},
{"exit",17},
{"hisir",18},
{"oyasumi",20}
};

//去掉string首尾空格
void trim(string& s)
{
if (!s.empty())
{
s.erase(0, s.find_first_not_of(" \t"));//首次出现不匹配空格的位置
s.erase(s.find_last_not_of(" \t") + 1);
}
}

//解析命令,cmdstr首位空格都去掉了
vector<string> parse(string cmdstr)
{
//对于一个关键字的命令,无法用空格分割,考虑到最后一定有个\n是没用的,因此把\n改为空格,一举两得
cmdstr[cmdstr.size() - 1] = ' ';
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = cmdstr.find(' ', pos)) != string::npos)
{
res.push_back(cmdstr.substr(pos, pos1 - pos));
while (cmdstr[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}

return res;

}

void run()
{

char cmdbuf[128];
char cmdtmp[128] = "no cmd";
bool reflag = false;
cout << "cmd> ";
//根据bool或运算顺序,如果用上一次的结果(reflag==true),就不接收字符
while (reflag || fgets(cmdbuf, sizeof(cmdbuf), stdin) != NULL)//gets已不被编译器支持,不太安全
{
string cmdstr(cmdbuf);
trim(cmdstr);

if (cmdstr == "\n")//不管怎么样都有个换行,仅有一个换行就不管
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << "cmd> ";
continue;
}
vector<string> cmdvec = parse(cmdstr);

//查找命令
auto iter = cmdmap.find(cmdvec[0]);
if (iter == cmdmap.end())//命令错误
{
cout << "Wrong command, your command parsed is: " << cmdvec[0] << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << "cmd> ";
continue;
}

int cmdvalue = iter->second;
switch (cmdvalue)
{
case 0:
if (cmdvec.size() != 4)
{
cout << "error> The number of parameters for [register] is wrong!" << endl;
break;
}
else if (cmdvec[2] != cmdvec[3])
{
cout << "error> The passwords entered twice are not equal!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 1:
if (cmdvec.size() != 3)
{
cout << "error> The number of parameters for [login] is wrong!" << endl;
break;
}

else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 2:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [search] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 3:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [chat] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 4:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [accept] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 5:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [reject] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 6:
if (cmdvec.size() != 1)
{
cout << "error> The number of parameters for [break] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << endl;
break;
}

case 7:
if (cmdvec.size() != 3)
{
cout << "error> The number of parameters for [send] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 8:
if (cmdvec.size() != 3)
{
cout << "error> The number of parameters for [sendfile] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 9:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [acceptfile] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 10:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [rejectfile] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 11:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [getfile] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 12:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [acceptget] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 13:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [rejectget] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 14:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [setsid] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 15:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [setsname] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 16:
if (cmdvec.size() != 1)
{
cout << "error> The number of parameters for [re] is wrong!" << endl;
reflag = false;//虽然是re,但是命令有问题
break;
}
else//即使是错误命令也存,用户可能头铁...就想再试一次
{
//做处理
if (!strcmp(cmdtmp, "no cmd"))//如果还没有命令
{
cout << "error> No command yet!" << endl;
}
else
{
cout << "reEX> " << cmdtmp;//不用换行,cmdtmp自然有个'\n'
//strcpy已不可用,不指定长度不安全。使用strlen要+1,因为长度不包含结束符,要补上去
strcpy_s(cmdbuf,strlen(cmdtmp)+1,cmdtmp);
reflag = true;//表明不必把cmdbuf置零
}
break;
}

case 17:
if (cmdvec.size() != 1)
{
cout << "error> The number of parameters for [exit] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << endl;
break;
}

case 18:
if (cmdvec.size() != 2)
{
cout << "error> The number of parameters for [hisir] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 20:
if (cmdvec.size() != 1)
{
cout << "error> The number of parameters for [oyasumi] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

}
if (cmdvalue == 16 && reflag)//如果是re且成功的话就不用cmd,不用cmdbuf置零
continue;
else if (cmdvalue == 16)//虽然是re但失败了,不能保存re
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << "cmd> ";
}
else
{
strcpy_s(cmdtmp,strlen(cmdbuf)+1,cmdbuf);//暂存上一次命令,方便re调用,如果上一次是re的话,不更新
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << "cmd> ";
reflag = false;//上一次不是re自然置false
}

}
}
int main()
{
run();
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
#include<iostream>
#include<cstring>
#include<WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton

#pragma comment(lib,"ws2_32.lib")//链接dll

//#define MYPORT 8000
//const char* SERVER_IP = "192.168.248.131";
//ip和port以参数形式传入
//INVALID_SOCKET(~0)和SOCKET_ERROR(-1)都是-1
SOCKET connect_S(const char* SERVER_IP, int MYPORT)//返回连接句柄
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
return INVALID_SOCKET;
}

//创建套接字
SOCKET connfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (connfd == INVALID_SOCKET)
{
std::cerr << "socket error !" << std::endl;
return INVALID_SOCKET;
}

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(MYPORT);//服务器端口,自己连接后的端口是os分配的,由进程选择一个端口去连服务器
//socketaddr.sin_addr.s_addr = inet_addr(SERVER_IP); ///服务器ip
//inet_addr最好换成inet_aton(),不会冤枉0.0.0.0和255.255.255.255
struct in_addr inaddr;
inet_pton(AF_INET, SERVER_IP, &inaddr);//windows下相当于inet_aton的函数,多了第一个参数表明是ipv4还是ipv6
socketaddr.sin_addr = inaddr;
std::cout << "connecting to ip-" << SERVER_IP << " port-" << MYPORT << std::endl;

///连接服务器,成功返回0,错误返回-1。返回的描述符connfd,该socket包含了服务器ip、port,自己ip、port,可用于发送和接收数据
if (connect(connfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == SOCKET_ERROR)
{
std::cerr << "connect fail !" << std::endl;
return SOCKET_ERROR;
}
std::cout << "connect to server successfully!" << std::endl;

return connfd;
}
/*
注意该函数还没有
closesocket(connfd);和WSACleanup();
我们写一个关闭函数
*/
//调用多少次connect_S,就要调用多少次close_S
void close_S(SOCKET connfd)
{
closesocket(connfd);
WSACleanup();
}

设计状态机

为了简化设计,有一些状态是显式状态,有一些状态是隐式状态。什么是显式和隐式呢?显式状态比如说在命令行和在chat时输入的响应是不一样的,这是全局的;隐式状态比如说一些等待状态,这些等待是阻塞的,局部的,就不需要显式切换状态。简单来说,

  • 显式状态:
    • 未登录状态:可注册、登录,其他命令都是ban的;登录后进入命令行状态;
    • 命令行状态:不可注册登录,其他可使用;chat后进入等待状态阻塞,对方接受后进入chat状态,否则返回命令行状态;
    • chat状态:此状态下的输入都发送给对端,除非使用@标识命令,比如使用@break退出chat状态;
    • 等待状态:所有等待状态下,可以输入@break返回命令行状态。
  • 隐式状态:
    • 请求获取等待状态:等待对方响应,响应后进入命令行状态(拒绝)或选择文件状态(接受);
    • 选择文件状态:等待用户输入文件序号;
    • chat等待状态:等待对方响应;
    • 发送文件等待状态:等待对方响应,响应后进入命令行状态;
    • 等待文件被获取状态:允许对方获取资源,等待对方选择文件。

状态使用枚举类,c++编程风格惯用法提出使用enum class代替enum和namespace,可以参考c++编程风格惯用法 | JySama

1
2
3
4
5
6
7
8
9
10
11
12
enum class clientState
{
noLogin = 0,//未登录状态
cmdLine,//命令行状态
isChatting,//chat状态
isWaiting//等待状态
};

//调用例子
clientState state;//定义一个枚举类型变量
if(state == clientState::noLogin and loginsuccess)
state = clientState::cmdLine;

状态机实现–重写run()

对于不同状态,我们希望输出的提示符不同,比如前面都是cmd> ,现在希望根据状态进行改变。一种方法是打印提示符时判断状态,太麻烦了;一种方法是把提示符看做变量,更改状态时就更改提示符。

1
2
3
4
5
6
7
8
9
10
11
12
命令行状态:"cmd> "
等待状态:"waiting> "
chat状态:"chatting with [sname]> "
未登录状态:"need login> "

unordered_map<clientState,string> promptMap =
{
{clientState::noLogin,"need login> "},
{clientState::cmdLine,"cmd> "},
{clientState::isChatting,"chatting with "},//进入chat状态要根据对方的sname来拼接
{clientState::isWaiting,"waiting> "}
};

如果没有登录而调用ban掉的命令,我们就打印一句错误信息

1
2
3
4
5
if(state == clientState::noLogin)
{
cerr << "error> Not signed in yet!" <<endl;
break;
}

如果已经登录还调用注册和登录命令,就打印一句错误信息

1
2
3
4
5
if(state != clientState::noLogin)
{
cerr << "error> Signed in yet!" <<endl;
break;
}

这些错误信息,我们可以在case语句里面加,但太臃肿了,我们进行一层封装

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
bool checkCmd(int cmdvalue)
{
//最开始只考虑是否登录
if(cmdvalue==0||cmdvalue==1)
{
if(state != clientState::noLogin)
{
cerr << "error> Have signed in yet!" <<endl;
return false;
}
}
else
{
if(state == clientState::noLogin)
{
cerr << "error> Not signed in yet!" <<endl;
return false;
}
}

//如果正在等待,只允许break
if(state == clientState::isWaiting && cmdvalue!= 6)
{
cerr << "error> Only break or @break can input because you are waiting for something!" <<endl;
return false;
}

return true;
}
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
//test
#include<iostream>
#include<cstring>//string.h
#include<string>
#include<unordered_map>
using namespace std;

const unordered_map<string, int> cmdmap =
{
{"register",0},
{"login",1},
{"search",2},
{"chat",3},
{"accept",4},
{"reject",5},
{"break",6},
{"send",7},
{"sendfile",8},
{"acceptfile",9},
{"rejectfile",10},
{"getfile",11},
{"acceptget",12},
{"rejectget",13},
{"setsid",14},
{"setsname",15},
{"re",16},
{"exit",17},
{"hisir",18},
{"oyasumi",20}
};


enum class clientState
{
noLogin = 0,//未登录状态
cmdLine,//命令行状态
isChatting,//chat状态
isWaiting//等待状态
};

unordered_map<clientState, string> promptMap =
{
{clientState::noLogin,"need login> "},
{clientState::cmdLine,"cmd> "},
{clientState::isChatting,"chatting with "},//进入chat状态要根据对方的sname来拼接
{clientState::isWaiting,"waiting> "}
};


//去掉string首尾空格
void trim(string& s)
{
if (!s.empty())
{
s.erase(0, s.find_first_not_of(" \t"));//首次出现不匹配空格的位置
s.erase(s.find_last_not_of(" \t") + 1);
}
}

//解析命令,cmdstr首位空格都去掉了
vector<string> parse(string cmdstr)
{
//对于一个关键字的命令,无法用空格分割,考虑到最后一定有个\n是没用的,因此把\n改为空格,一举两得
cmdstr[cmdstr.size() - 1] = ' ';
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = cmdstr.find(' ', pos)) != string::npos)
{
res.push_back(cmdstr.substr(pos, pos1 - pos));
while (cmdstr[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}

return res;

}
clientState state = clientState::noLogin;
string prompt = promptMap[state];
bool checkCmd(int cmdvalue)
{
//最开始只考虑是否登录
if (cmdvalue == 0 || cmdvalue == 1)
{
if (state != clientState::noLogin)
{
cerr << "error> Have signed in yet!" << endl;
return false;
}
}
else
{
if (state == clientState::noLogin)
{
cerr << "error> Not signed in yet!" << endl;
return false;
}
}

//如果正在等待,只允许break
if (state == clientState::isWaiting && cmdvalue != 6)
{
cerr << "error> Only break or @break can input because you are waiting for something!" << endl;
return false;
}

return true;
}



void run()
{

char cmdbuf[128];
char cmdtmp[128] = "no cmd";
bool reflag = false;
cout << prompt;
//根据bool或运算顺序,如果用上一次的结果(reflag==true),就不接收字符
while (reflag || fgets(cmdbuf, sizeof(cmdbuf), stdin) != NULL)//gets已不被编译器支持,不太安全
{
string cmdstr(cmdbuf);
trim(cmdstr);

if (cmdstr == "\n")//不管怎么样都有个换行,仅有一个换行就不管
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt;
continue;
}
vector<string> cmdvec = parse(cmdstr);

//查找命令
auto iter = cmdmap.find(cmdvec[0]);
if (iter == cmdmap.end())//命令错误
{
cerr << "Wrong command, your command parsed is: " << cmdvec[0] << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
reflag = false;//因为可能是调用了re(连续两次login)会导致错误,这里错误要让用户重新输入,否则会崩溃(cmdbuf是空)
cout << prompt;
continue;
}

int cmdvalue = iter->second;
if (!checkCmd(cmdvalue))//如果命令与状态不匹配
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt;
continue;
}
switch (cmdvalue)
{
case 0:
if (cmdvec.size() != 4)
{
cerr << "error> The number of parameters for [register] is wrong!" << endl;
break;
}
else if (cmdvec[2] != cmdvec[3])
{
cerr << "error> The passwords entered twice are not equal!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 1:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [login] is wrong!" << endl;
break;
}

else
{
//做处理
state = clientState::cmdLine;
prompt = promptMap[state];
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 2:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [search] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 3:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [chat] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input break or @break to back to command line..." << endl;
break;
}

case 4:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [accept] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 5:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [reject] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 6:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [break] is wrong!" << endl;
break;
}
else
{
//做处理
state = clientState::cmdLine;
prompt = promptMap[state];
break;
}

case 7:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [send] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 8:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [sendfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 9:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptfile] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 10:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectfile] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 11:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [getfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 12:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptget] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 13:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectget] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 14:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsid] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 15:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsname] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 16:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [re] is wrong!" << endl;
reflag = false;//虽然是re,但是命令有问题
break;
}
else//即使是错误命令也存,用户可能头铁...就想再试一次
{
//做处理
if (!strcmp(cmdtmp, "no cmd"))//如果还没有命令
{
cerr << "error> No command yet!" << endl;
}
else
{
cout << "reEX> " << cmdtmp;//不用换行,cmdtmp自然有个'\n'
//strcpy已不可用,不指定长度不安全。使用strlen要+1,因为长度不包含结束符,要补上去
strcpy_s(cmdbuf, strlen(cmdtmp) + 1, cmdtmp);
reflag = true;//表明不必把cmdbuf置零
}
break;
}

case 17:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [exit] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << endl;
break;
}

case 18:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [hisir] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 20:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [oyasumi] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

}
if (cmdvalue == 16 && reflag)//如果是re且成功的话就不用cmd,不用cmdbuf置零
continue;
else if (cmdvalue == 16)//虽然是re但失败了,不能保存re
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt;
}
else
{
strcpy_s(cmdtmp, strlen(cmdbuf) + 1, cmdbuf);//暂存上一次命令,方便re调用,如果上一次是re的话,不更新
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt;
reflag = false;//上一次不是re自然置false
}

}
}

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

处理@

我们对命令的解析还没有包含@形式,实际上@只有在chatting状态下才被必须,其他情况下只需要简单的把@去掉即可,即检测到第一个字符的@的话就把@去掉。

而在chatting状态下,我们依旧从fgets里接收输入,如果第一个字符是@,我们判定为输入命令,这样才有一系列的操作(解析命令、进行switch等等):

1
2
3
4
5
6
7
8
9
10
if(state == clientState::isChatting && cmdbuf[0]!='@')//如果在chatting,并且输入第一个不是@,判定为聊天内容直接发送
{
//send...
cout << prompt;
}

else//否则一定是命令
{
//解析命令
}

这样,对于解析命令函数,就可以这样改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vector<string> parse(string cmdstr)
{
//首先看第一个字符是不是@,是的话去掉就好了
if(cmdstr[0] == '@')
cmdstr = cmdstr.substr(1,cmdstr.size()-1);

//对于一个关键字的命令,无法用空格分割,考虑到最后一定有个\n是没用的,因此把\n改为空格,一举两得
cmdstr[cmdstr.size() - 1] = ' ';
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = cmdstr.find(' ', pos)) != string::npos)
{
res.push_back(cmdstr.substr(pos, pos1 - pos));
while (cmdstr[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}

return res;

}

我们还没有处理chatting的状态转换,因为这涉及服务器发回确认,现在我们在chat命令那使用Sleep()函数阻塞一段时间,表示等待同意。demo可以这样改:

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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
//test
#include<iostream>
#include<cstring>//string.h
#include<string>
#include<unordered_map>
#include <windows.h>//Sleep()
using namespace std;

string chatSname = "";

const unordered_map<string, int> cmdmap =
{
{"register",0},
{"login",1},
{"search",2},
{"chat",3},
{"accept",4},
{"reject",5},
{"break",6},
{"send",7},
{"sendfile",8},
{"acceptfile",9},
{"rejectfile",10},
{"getfile",11},
{"acceptget",12},
{"rejectget",13},
{"setsid",14},
{"setsname",15},
{"re",16},
{"exit",17},
{"hisir",18},
{"oyasumi",20}
};


enum class clientState
{
noLogin = 0,//未登录状态
cmdLine,//命令行状态
isChatting,//chat状态
isWaiting//等待状态
};

unordered_map<clientState, string> promptMap =
{
{clientState::noLogin,"need login> "},
{clientState::cmdLine,"cmd> "},
{clientState::isChatting,"chatting with "},//进入chat状态要根据对方的sname来拼接
{clientState::isWaiting,"waiting> "}
};


//去掉string首尾空格
void trim(string& s)
{
if (!s.empty())
{
s.erase(0, s.find_first_not_of(" \t"));//首次出现不匹配空格的位置
s.erase(s.find_last_not_of(" \t") + 1);
}
}

//解析命令,cmdstr首位空格都去掉了
vector<string> parse(string cmdstr)
{
//首先看第一个字符是不是@,是的话去掉就好了
if (cmdstr[0] == '@')
cmdstr = cmdstr.substr(1, cmdstr.size() - 1);

//对于一个关键字的命令,无法用空格分割,考虑到最后一定有个\n是没用的,因此把\n改为空格,一举两得
cmdstr[cmdstr.size() - 1] = ' ';
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = cmdstr.find(' ', pos)) != string::npos)
{
res.push_back(cmdstr.substr(pos, pos1 - pos));
while (cmdstr[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}

return res;

}
clientState state = clientState::noLogin;
string prompt = promptMap[state];
bool checkCmd(int cmdvalue)
{
//最开始只考虑是否登录
if (cmdvalue == 0 || cmdvalue == 1)
{
if (state != clientState::noLogin)
{
cerr << "error> Have signed in yet!" << endl;
return false;
}
}
else
{
if (state == clientState::noLogin)
{
cerr << "error> Not signed in yet!" << endl;
return false;
}
}
//如果正在chatting,不允许再chat
if (state == clientState::isChatting && cmdvalue == 3)
{
cerr << "error> You are chatting with "<<chatSname<<"! You can break to chat with other." << endl;
return false;
}

//如果正在等待,只允许break
if (state == clientState::isWaiting && cmdvalue != 6)
{
cerr << "error> Only break or @break can input because you are waiting for something!" << endl;
return false;
}

return true;
}



void run()
{

char cmdbuf[128];
char cmdtmp[128] = "no cmd";
bool reflag = false;
cout << prompt;
//根据bool或运算顺序,如果用上一次的结果(reflag==true),就不接收字符
while (reflag || fgets(cmdbuf, sizeof(cmdbuf), stdin) != NULL)//gets已不被编译器支持,不太安全
{
if (state == clientState::isChatting && cmdbuf[0] != '@')//如果在chatting,并且输入第一个不是@,判定为聊天内容直接发送
{
//send...
cout << "send something..." << endl;//test
cout << prompt;
}
else//剩下的内容都是else的
{
string cmdstr(cmdbuf);
trim(cmdstr);

if (cmdstr == "\n")//不管怎么样都有个换行,仅有一个换行就不管
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt;
continue;
}
vector<string> cmdvec = parse(cmdstr);

//查找命令
auto iter = cmdmap.find(cmdvec[0]);
if (iter == cmdmap.end())//命令错误
{
cerr << "Wrong command, your command parsed is: " << cmdvec[0] << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt;
continue;
}

int cmdvalue = iter->second;
if (!checkCmd(cmdvalue))//如果命令与状态不匹配
{
memset(cmdbuf, 0, sizeof(cmdbuf));
reflag = false;//因为可能是调用了re(连续两次login)会导致错误,这里错误要让用户重新输入,否则会崩溃(cmdbuf是空)
cout << prompt;
continue;
}
switch (cmdvalue)
{
case 0:
if (cmdvec.size() != 4)
{
cerr << "error> The number of parameters for [register] is wrong!" << endl;
break;
}
else if (cmdvec[2] != cmdvec[3])
{
cerr << "error> The passwords entered twice are not equal!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 1:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [login] is wrong!" << endl;
break;
}

else
{
//做处理
state = clientState::cmdLine;
prompt = promptMap[state];
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 2:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [search] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 3:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [chat] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input break or @break to back to command line..." << endl;

Sleep(3000);//阻塞3秒,假装等待连接
state = clientState::isChatting;
//chatSname = ...
prompt = promptMap[state] + chatSname+"> ";
break;
}

case 4:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [accept] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 5:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [reject] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 6:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [break] is wrong!" << endl;
break;
}
else
{
//做处理
state = clientState::cmdLine;
prompt = promptMap[state];
break;
}

case 7:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [send] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 8:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [sendfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 9:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptfile] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 10:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectfile] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 11:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [getfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 12:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptget] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 13:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectget] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 14:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsid] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 15:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsname] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 16:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [re] is wrong!" << endl;
reflag = false;//虽然是re,但是命令有问题
break;
}
else//即使是错误命令也存,用户可能头铁...就想再试一次
{
//做处理
if (!strcmp(cmdtmp, "no cmd"))//如果还没有命令
{
cerr << "error> No command yet!" << endl;
}
else
{
cout << "reEX> " << cmdtmp;//不用换行,cmdtmp自然有个'\n'
//strcpy已不可用,不指定长度不安全。使用strlen要+1,因为长度不包含结束符,要补上去
strcpy_s(cmdbuf, strlen(cmdtmp) + 1, cmdtmp);
reflag = true;//表明不必把cmdbuf置零
}
break;
}

case 17:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [exit] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << endl;
break;
}

case 18:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [hisir] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 20:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [oyasumi] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

}
if (cmdvalue == 16 && reflag)//如果是re且成功的话就不用cmd,不用cmdbuf置零
continue;
else if (cmdvalue == 16)//虽然是re但失败了,不能保存re
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt;
}
else
{
strcpy_s(cmdtmp, strlen(cmdbuf) + 1, cmdbuf);//暂存上一次命令,方便re调用,如果上一次是re的话,不更新
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt;
reflag = false;//上一次不是re自然置false
}
}


}
}

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

接收处理

在基础设计时,已经降低了交互的复杂度,比如我们使用accept命令来回复,而不是直接根据请求键入其他东西,在接下来的考虑中,你会得到更深刻的理解。尽管我们仍然有许多命令没处理完,但在处理它们之前,要讨论怎么从服务器接收返回的数据,这是个很值得深思的问题。

对于发送命令来说,我们在本地解析了命令就可以直接发送给服务器,服务器根据前面设计的命令解析(只有少许不同,让服务器区分发送的是命令还是聊天内容)就可以解析完成,因此对于客户端来说,重点是处理接收,困难就在于:用户只有一个输出缓冲区和一个输入缓冲区!

  • 一种方式是:让用户在调用需要服务器交互的命令后阻塞,等待接收,然后对接收内容进行处理。比如当login、register后,等待服务器返回成功和失败信息即可,理论上所有的命令都可以用这种方式来实现;
  • 上面的方式没有考虑一个问题,就是当别的客户发来一条信息时,实际上本地客户端并不一定去recv。比如本地用户还在干别的事情就有别的用户发一个send或get请求过来,这个请求经由服务器再发送到本地客户端,直到下次本地客户调用阻塞命令才会进行接收。另外在chat的时候实际上也不知道什么时候该recv。一方面时延很久,一方面无法区分返回的命令。这是两个很严重的问题。
  • 额外接收线程:我们还是对问题简化
    • 直接与服务器交互的命令,就直接阻塞等待接收(因为必须等待处理);
    • 与其他用户的交互,这会导致信息不知道什么时候发送过来,就另外用一个线程去接收,这个线程使用另外一个套接字(线程的port也会不一样),发送给哪个套接字交给服务器判断就好了。

  • 看起来问题已经解决了,但如果服务器只有一个监听端口,会使得复杂度过大。服务器是可以监听多个端口的,那么可以让服务器用两个监听线程。这两个监听线程刚好可以把两种套接字区分开来。
  • 由于有两个监听线程,导致我们不得不再考虑:什么信息该发送到哪个端口?这个问题产生的原因是,两个监听线程得到的连接套接字可以要交互。比如chat,当用户1发送一条消息时,实际上是在我们的主线程(接收stdin解析命令线程)发送的,如果走的是监听1(接收命令的线程),那么要想把这条消息发送给用户2,就要从监听2那里获取用户2的接收信息套接字,再发送。这使得问题复杂化,可能需要考虑共享变量互斥的问题。因此我们不得不考虑发送什么到哪里的问题。
  • 好吧,其实这个问题不用考虑:“接收线程允许发送命令吗?”这是不太允许的,因为它要一直recv,你可能会说用非阻塞的recv,但这又使得问题走向另一个复杂的地方。我们可以断言:所有的命令,都要发送给服务器的接收命令的线程!至于命令或是回复的转交,不得不通过另一个连接来执行。

现在来完成接收线程要做的事情,先封装到一个函数里,这里首先要连接服务器,还记得前面写的connect_S函数吗:

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
#include<iostream>
#include<cstring>
#include<WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton

#pragma comment(lib,"ws2_32.lib")//链接dll

//#define MYPORT 8000
//const char* SERVER_IP = "192.168.248.131";
//ip和port以参数形式传入
//INVALID_SOCKET(~0)和SOCKET_ERROR(-1)都是-1
SOCKET connect_S(const char* SERVER_IP, int MYPORT)//返回连接句柄
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
return INVALID_SOCKET;
}

//创建套接字
SOCKET connfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (connfd == INVALID_SOCKET)
{
std::cerr << "socket error !" << std::endl;
return INVALID_SOCKET;
}

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(MYPORT);//服务器端口,自己连接后的端口是os分配的,由进程选择一个端口去连服务器
//socketaddr.sin_addr.s_addr = inet_addr(SERVER_IP); ///服务器ip
//inet_addr最好换成inet_aton(),不会冤枉0.0.0.0和255.255.255.255
struct in_addr inaddr;
inet_pton(AF_INET, SERVER_IP, &inaddr);//windows下相当于inet_aton的函数,多了第一个参数表明是ipv4还是ipv6
socketaddr.sin_addr = inaddr;
std::cout << "connecting to ip-" << SERVER_IP << " port-" << MYPORT << std::endl;

///连接服务器,成功返回0,错误返回-1。返回的描述符connfd,该socket包含了服务器ip、port,自己ip、port,可用于发送和接收数据
if (connect(connfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == SOCKET_ERROR)
{
std::cerr << "connect fail !" << std::endl;
return SOCKET_ERROR;
}
std::cout << "connect to server successfully!" << std::endl;

return connfd;
}
/*
注意该函数还没有
closesocket(connfd);和WSACleanup();
我们写一个关闭函数
*/
//调用多少次connect_S,就要调用多少次close_S
void close_S(SOCKET connfd)
{
closesocket(connfd);
WSACleanup();
}

这样我们的接收线程的工作函数的工作逻辑就更清楚了:

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
void recvThread(const char* SERVER_IP, int MYPORT)
{
//连接服务器
SOCKET connfd = connect_S(SERVER_IP,MYPORT);//获取套接字
if(connfd==INVALID_SOCKET || connfd==SOCKET_ERROR)
exit(1);//连接失败退出,错误由connect_S函数输出

//--------------------------工作-----------------------------
char recvbuf[1024];
while(1)
{
memset(recvbuf,0,sizeof(recvbuf));//每次都将buffer清空,防止被上次写入的结果影响
size_t recvbytes = recv(connfd, recvbuf, sizeof(recvbuf),0);//同步接收,是阻塞的
//异常结束时,退出
if(recvbytes<=0)
{
cout<<"The receiving thread exits!"<<endl;
break;
}

cout<<recvbuf<<endl;//endl可以刷新缓冲
}
//退出
close_S(connfd);
}

接收线程的工作大概就是这些,只是简单的打印,因为我们把复杂度交给服务器了。不过,我们考虑下主线程和接收线程之间的关系——它们共享输出缓冲。因此会发生一种情况,在主线程打印出来的提示符会被顶掉,尽管用户仍然可以在主线程输入,但会让用户感觉“坏掉了”,大概像这样:

1
2
cmd> something....recv> .....................
here enter other

因此,除了打印前需要换行,还需要在打印后把提示符再打印出来:

1
cout<<endl<<recvbuf<<endl<<prompt;

然后加入线程,需要头文件<thread><functional>

1
thread(bind(recvThread, SERVER_IP, MYPORT)).detach();

阶段性测试的demo如下,我们把接收线程相关的函数都放在main函数前面,因为已经相当臃肿了,等处理完一并封装。注意这里面有<windows.h><WinSock2.h>,后者一定要在前者前include。

1
2
3
“winsock2.h”定义了_WINSOCKAPI_,原意是不要编译''winsock.h"。
如果"windows.h"在“winsock2.h”前面,那么会先include “winsock.h”编译,后再“winsock2.h”时遇到了重定义。
所以先include “winsock2.h”再include "windows.h"就可以避开这些重定义。

同时,我们进行一点优化,以往打印提示符时没有刷新缓冲区,现在我们把刷新缓冲加上,否则可能会堵住显得卡,像这样cout << prompt << flush;

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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
//test
#include<iostream>
#include<cstring>//string.h
#include<string>
#include<unordered_map>

#include <thread>
#include<functional>
#include<WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton
#include <windows.h>//Sleep()

#pragma comment(lib,"ws2_32.lib")//链接dll
using namespace std;

string chatSname = "";

const unordered_map<string, int> cmdmap =
{
{"register",0},
{"login",1},
{"search",2},
{"chat",3},
{"accept",4},
{"reject",5},
{"break",6},
{"send",7},
{"sendfile",8},
{"acceptfile",9},
{"rejectfile",10},
{"getfile",11},
{"acceptget",12},
{"rejectget",13},
{"setsid",14},
{"setsname",15},
{"re",16},
{"exit",17},
{"hisir",18},
{"oyasumi",20}
};


enum class clientState
{
noLogin = 0,//未登录状态
cmdLine,//命令行状态
isChatting,//chat状态
isWaiting//等待状态
};

unordered_map<clientState, string> promptMap =
{
{clientState::noLogin,"need login> "},
{clientState::cmdLine,"cmd> "},
{clientState::isChatting,"chatting with "},//进入chat状态要根据对方的sname来拼接
{clientState::isWaiting,"waiting> "}
};


//去掉string首尾空格
void trim(string& s)
{
if (!s.empty())
{
s.erase(0, s.find_first_not_of(" \t"));//首次出现不匹配空格的位置
s.erase(s.find_last_not_of(" \t") + 1);
}
}

//解析命令,cmdstr首位空格都去掉了
vector<string> parse(string cmdstr)
{
//首先看第一个字符是不是@,是的话去掉就好了
if (cmdstr[0] == '@')
cmdstr = cmdstr.substr(1, cmdstr.size() - 1);

//对于一个关键字的命令,无法用空格分割,考虑到最后一定有个\n是没用的,因此把\n改为空格,一举两得
cmdstr[cmdstr.size() - 1] = ' ';
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = cmdstr.find(' ', pos)) != string::npos)
{
res.push_back(cmdstr.substr(pos, pos1 - pos));
while (cmdstr[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}

return res;

}
clientState state = clientState::noLogin;
string prompt = promptMap[state];
bool checkCmd(int cmdvalue)
{
//最开始只考虑是否登录
if (cmdvalue == 0 || cmdvalue == 1)
{
if (state != clientState::noLogin)
{
cerr << "error> Have signed in yet!" << endl;
return false;
}
}
else
{
if (state == clientState::noLogin)
{
cerr << "error> Not signed in yet!" << endl;
return false;
}
}
//如果正在chatting,不允许再chat
if (state == clientState::isChatting && cmdvalue == 3)
{
cerr << "error> You are chatting with " << chatSname << "! You can break to chat with other." << endl;
return false;
}

//如果正在等待,只允许break
if (state == clientState::isWaiting && cmdvalue != 6)
{
cerr << "error> Only break or @break can input because you are waiting for something!" << endl;
return false;
}

return true;
}



void run()
{

char cmdbuf[128];
char cmdtmp[128] = "no cmd";
bool reflag = false;
cout << prompt << flush;
//根据bool或运算顺序,如果用上一次的结果(reflag==true),就不接收字符
while (reflag || fgets(cmdbuf, sizeof(cmdbuf), stdin) != NULL)//gets已不被编译器支持,不太安全
{
if (state == clientState::isChatting && cmdbuf[0] != '@')//如果在chatting,并且输入第一个不是@,判定为聊天内容直接发送
{
//send...
cout << "send something..." << endl;//test
cout << prompt << flush;
}
else//剩下的内容都是else的
{
string cmdstr(cmdbuf);
trim(cmdstr);

if (cmdstr == "\n")//不管怎么样都有个换行,仅有一个换行就不管
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}
vector<string> cmdvec = parse(cmdstr);

//查找命令
auto iter = cmdmap.find(cmdvec[0]);
if (iter == cmdmap.end())//命令错误
{
cerr << "Wrong command, your command parsed is: " << cmdvec[0] << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}

int cmdvalue = iter->second;
if (!checkCmd(cmdvalue))//如果命令与状态不匹配
{
memset(cmdbuf, 0, sizeof(cmdbuf));
reflag = false;//因为可能是调用了re(连续两次login)会导致错误,这里错误要让用户重新输入,否则会崩溃(cmdbuf是空)
cout << prompt << flush;
continue;
}
switch (cmdvalue)
{
case 0:
if (cmdvec.size() != 4)
{
cerr << "error> The number of parameters for [register] is wrong!" << endl;
break;
}
else if (cmdvec[2] != cmdvec[3])
{
cerr << "error> The passwords entered twice are not equal!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 1:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [login] is wrong!" << endl;
break;
}

else
{
//做处理
state = clientState::cmdLine;
prompt = promptMap[state];
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 2:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [search] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 3:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [chat] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input break or @break to back to command line..." << endl;

Sleep(3000);//阻塞3秒,假装等待连接
state = clientState::isChatting;
//chatSname = ...
prompt = promptMap[state] + chatSname + "> ";
break;
}

case 4:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [accept] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 5:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [reject] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 6:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [break] is wrong!" << endl;
break;
}
else
{
//做处理
state = clientState::cmdLine;
prompt = promptMap[state];
break;
}

case 7:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [send] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << " " << cmdvec[2] << endl;
break;
}

case 8:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [sendfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 9:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptfile] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 10:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectfile] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 11:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [getfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 12:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptget] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 13:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectget] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 14:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsid] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 15:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsname] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 16:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [re] is wrong!" << endl;
reflag = false;//虽然是re,但是命令有问题
break;
}
else//即使是错误命令也存,用户可能头铁...就想再试一次
{
//做处理
if (!strcmp(cmdtmp, "no cmd"))//如果还没有命令
{
cerr << "error> No command yet!" << endl;
}
else
{
cout << "reEX> " << cmdtmp;//不用换行,cmdtmp自然有个'\n'
//strcpy已不可用,不指定长度不安全。使用strlen要+1,因为长度不包含结束符,要补上去
strcpy_s(cmdbuf, strlen(cmdtmp) + 1, cmdtmp);
reflag = true;//表明不必把cmdbuf置零
}
break;
}

case 17:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [exit] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << endl;
break;
}

case 18:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [hisir] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

case 20:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [oyasumi] is wrong!" << endl;
break;
}
else
{
//做处理
cout << "test> " << cmdvec[0] << " " << cmdvec[1] << endl;
break;
}

}
if (cmdvalue == 16 && reflag)//如果是re且成功的话就不用cmd,不用cmdbuf置零
continue;
else if (cmdvalue == 16)//虽然是re但失败了,不能保存re
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
}
else
{
strcpy_s(cmdtmp, strlen(cmdbuf) + 1, cmdbuf);//暂存上一次命令,方便re调用,如果上一次是re的话,不更新
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
reflag = false;//上一次不是re自然置false
}
}


}
}
SOCKET connect_S(const char* SERVER_IP, int MYPORT)//返回连接句柄
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
return INVALID_SOCKET;
}

//创建套接字
SOCKET connfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (connfd == INVALID_SOCKET)
{
std::cerr << "socket error !" << std::endl;
return INVALID_SOCKET;
}

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(MYPORT);//服务器端口,自己连接后的端口是os分配的,由进程选择一个端口去连服务器
//socketaddr.sin_addr.s_addr = inet_addr(SERVER_IP); ///服务器ip
//inet_addr最好换成inet_aton(),不会冤枉0.0.0.0和255.255.255.255
struct in_addr inaddr;
inet_pton(AF_INET, SERVER_IP, &inaddr);//windows下相当于inet_aton的函数,多了第一个参数表明是ipv4还是ipv6
socketaddr.sin_addr = inaddr;

///连接服务器,成功返回0,错误返回-1。返回的描述符connfd,该socket包含了服务器ip、port,自己ip、port,可用于发送和接收数据
if (connect(connfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == SOCKET_ERROR)
{
std::cerr << "connect fail !" << std::endl;
return SOCKET_ERROR;
}
return connfd;
}
/*
注意该函数还没有
closesocket(connfd);和WSACleanup();
我们写一个关闭函数
*/
//调用多少次connect_S,就要调用多少次close_S
void close_S(SOCKET connfd)
{
closesocket(connfd);
WSACleanup();
}
void recvThread(const char* SERVER_IP, int MYPORT)
{
//连接服务器
SOCKET connfd = connect_S(SERVER_IP, MYPORT);//获取套接字
if (connfd == INVALID_SOCKET || connfd == SOCKET_ERROR)
exit(1);//连接失败退出,错误由connect_S函数输出

//--------------------------工作-----------------------------
char recvbuf[1024];
while (1)//后面会设置exitflag
{
memset(recvbuf, 0, sizeof(recvbuf));//每次都将buffer清空,防止被上次写入的结果影响
size_t recvbytes = recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
//异常结束时,退出
if (recvbytes <= 0)
{
cout << endl << "The receiving thread exits!" << endl << prompt << flush;
break;
}

cout << endl << recvbuf << endl << prompt << flush;
}
//退出
close_S(connfd);
}
const char* SERVER_IP = "192.168.248.131";
int MYPORT = 8000;

int main()
{
thread(bind(recvThread, SERVER_IP, MYPORT)).detach();
run();
return 0;
}

简单写一个linux服务器发送数据测试,有了这台服务器,我们之后就可以模拟响应往下做了,尽管我们现在是手动在服务器上发送数据。

服务器发送exit就可以退出,接收线程也会因此退出,我们现在可以先放着,不去管它。

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
//server
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <string.h>
#include <iostream>//cerr
#include <unistd.h>//close
#define myport 8000


int main()
{
//定义socketfd,它要绑定监听的网卡地址和端口
int listenfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(myport);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;
//因为路由的关系,从客户端来的IP包只可能到达其中一个网卡。指定了网卡地址的话,必须从相应的地址进入才能连接到port
//#define INADDR_ANY ((in_addr_t) 0x00000000)

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if(bind(listenfd,(struct sockaddr *)&socketaddr,sizeof(socketaddr))==-1)
{
std::cerr<<"bind"<<std::endl;
exit(1);
//cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲.
//也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。
}
std::cout<<"listen socket port: "<<myport<<std::endl;

//开始监听
if(listen(listenfd,SOMAXCONN) == -1)
{
std::cerr<<"listen"<<std::endl;
exit(1);
}

///客户端套接字
char buffer[1024];
struct sockaddr_in client_addr;//获取客户的地址和端口号,连接后的不分配新端口
socklen_t len = sizeof(client_addr);//socklen_t 相当于 int,但使用int必须强制转型告知编译器
std::cout<<"wating for conn..."<<std::endl;

int conn = accept(listenfd, (struct sockaddr*)&client_addr, &len);//阻塞,等待连接,成功则创建连接套接字conn描述这个用户
if(conn==-1)
{
std::cerr<<"connect"<<std::endl;
exit(1);
}
/*
如果队列中没有等待的连接,套接字也没有被标记为Non-blocking,accept()会阻塞调用函数直到连接出现;
如果套接字被标记为Non-blocking,队列中也没有等待的连接,accept()返回错误EAGAIN或EWOULDBLOCK。
*/
std::cout<<"conn successfully: port-"<<ntohs(client_addr.sin_port)<<" ip-"<<inet_ntoa(client_addr.sin_addr)<<std::endl<<std::endl;
char sendbuf[1024];
//gets:从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时停止
//如果成功,该函数返回 str。如果发生错误或者到达文件末尾时还未读取任何字符,则返回 NULL。
//等价于fgets(sendbuf, sizeof(sendbuf), stdin)

std::cout<<"send> ";
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)//gets已不被编译器支持,不太安全
{
std::cout<<"send to client: "<<sendbuf;//不用换行fgets包含了\n
send(conn, sendbuf, strlen(sendbuf)-1,0); ///\n is ignored
if(strcmp(sendbuf,"exit\n")==0)
break;

bzero(sendbuf,sizeof(sendbuf));

std::cout<<"send> ";
}

close(conn);
close(listenfd);
return 0;
}

命令发送与接收

现在我们来处理主线程的命令发送,基本上就是处理run方法,不过注意,在main调用run前,我们先进行连接,然后把套接字传给run(),大概像这样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
//连接服务器
SOCKET connfd = connect_S(SERVER_IP, MYPORT);//获取套接字
if (connfd == INVALID_SOCKET || connfd == SOCKET_ERROR)
exit(1);//连接失败退出,错误由connect_S函数输出

run(connfd);

//退出
close_S(connfd);
return 0;
}

现在我们对run函数作修改,注意一个前提,进入switch内的else的部分,都是可以进行执行的正确的命令,这些命令可以发送给服务器。我们对这些部分进行send和recv处理。关于recv,在主线程中是阻塞我们的输入的,但是当我们在waiting时,还是能够输入break来退出等待(这是必要的),这就不能阻塞了,因此对于waiting的这部分,接收放到接收线程处理。

为什么说break是必要的呢?我们实际上可以认为服务器是不会让用户等(或者说等太久)的,但对等方是可能让本地用户等很久的(可能由于意外、可能由于心理道德),这就需要允许用户自主退出,尽管这会增加许多复杂度。


对于那些要接收回复的命令,我们在此探讨一下它们要接收什么。

  • register
    • 注册正确还是错误:success | failure
    • 如果是success,在本地处理并返回注册正确的信息;
    • 如果是failure,在本地处理并返回注册失败的信息,由于我们判断了密码相不相同了,这必然是用户ID已被使用了;
  • login
    • 如果是success,在本地处理并返回登录成功的信息,并进入命令行;
    • 如果是failure,可能是密码错了或者根本没有用户ID,这时服务器不返回failure,而是返回相关的错误信息,直接打印即可;
  • search
    • 如果是failure,本地处理并返回不存在该sid
    • 否则成功,服务器直接发回sname和是否在chat的信息,直接打印
  • chat
    • 服务器返回对方是否接受的信息,为了区别格式(命令与命令之间,命令与聊天信息之间),服务器会发送这样的信息:”@#chat accept”、”@#chat reject”
    • 然后会进行状态的更新
  • accept
    • 暂不考虑交互,但可能因为时延影响,后续也许涉及交互的认证
  • reject
    • 与accept一样
  • break
    • 与accept一样
  • send
    • 这里降低复杂度,如果对方sid不存在也不返回消息,所以需要用户先search
  • sendfile
    • 服务器返回对方是否接受的信息,为了区别格式,服务器会发送这样的信息:”@#sendfile accept”、”@#sendfile reject”
  • acceptfile
    • 与accept一样
  • rejectfile
    • 与accept一样
  • getfile
    • 当对方acceptget时,会给本地用户一个资源表,因此服务器发回来的就是这样一个资源表。
    • 那么服务器返回的消息像这样:”@#getfile reject”、”@#getfile accept resource_list”
  • acceptget
    • 不阻塞直接可以做别的事,即一定允许发送,接收线程处理对方选择,服务器返回这样一条信息:”@#choosefile [number]”
  • rejectget
    • 与accept一样
  • setsid
    • 返回成功和失败信息,直接打印
  • setsname
    • 返回成功和失败信息,直接打印
  • re
    • 不返回
  • exit
    • 不返回
  • hisir
    • 返回一句话,直接打印
  • oyasumi
    • 会返回一句晚安消息(随机选择),然后打印

有些返回是直接打印的,而有些返回需要进行解析,比如login;而那些accept之类的,因为主线程是在waiting状态,那就让接收线程解析。

当用户在选择文件时,要键入输入,为了不和主线程的输入冲突,我们就在主线程输入。为了不增加状态(比如目前在选择文件状态),我们只需要增加一个选择文件flag就好了,如果是true的话表明这个输入用于选择文件。发送的命令是:choosefile [number],下面的run先不对此做修改

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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
bool chooseflag = false;
bool isNumber(const char* buf)//判断字符传是否为数字
{
for(int i=0;buf[i]!='\n';i++)//以\n结尾
if(buf[i]<'0'||buf[i]>'9')//每个字符都要是数字
return false;
return true;
}
string chattarg = "";
void run(SOCKET connfd)
{
//一个汉字两字节,可能需要更大的buf
char cmdbuf[256];
char cmdtmp[256] = "no cmd";
char recvbuf[256];
bool reflag = false;
bool exitflag = false;//函数退出信号
cout << prompt << flush;
//根据bool或运算顺序,如果用上一次的结果(reflag==true),就不接收字符
while (reflag || fgets(cmdbuf, sizeof(cmdbuf), stdin) != NULL)//gets已不被编译器支持,不太安全
{
if(chooseflag)//如果在选择文件。接收线程会把这个flag改成true,并且打印提示符enter file number>
{
if(isNumber(cmdbuf))
{
//要进行数字的范围判断,用stoi
int num = stoi(cmdbuf);
if(num>fileNumber || num<1)
{
cerr << "Please enter a number in the range!" << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt<< flush;
continue;
}
send(connfd, cmdbuf, sizeof(cmdbuf),0);//发送
memset(cmdbuf, 0, sizeof(cmdbuf));
//...还需要用一个线程来接收
cout << "Downloading..." << endl;
chooseflag = false;
state = clientState::cmdLine;
prompt = promptMap[state];
cout << prompt << flush;
continue;
}

else
{
cerr << "Please enter a number!" << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt<< flush;
continue;
}
}
if (state == clientState::isChatting && cmdbuf[0] != '@')//如果在chatting,并且输入第一个不是@,判定为聊天内容直接发送
{
//send...
send(connfd, cmdbuf, sizeof(cmdbuf),0);//发送
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
}
else//剩下的内容都是else的
{
memset(recvbuf, 0, sizeof(recvbuf));//把接收缓冲清零
string cmdstr(cmdbuf);
trim(cmdstr);

if (cmdstr == "\n")//不管怎么样都有个换行,仅有一个换行就不管
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}
vector<string> cmdvec = parse(cmdstr);

//查找命令
auto iter = cmdmap.find(cmdvec[0]);
if (iter == cmdmap.end())//命令错误
{
cerr << "Wrong command, your command parsed is: " << cmdvec[0] << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}

int cmdvalue = iter->second;
if (!checkCmd(cmdvalue))//如果命令与状态不匹配
{
memset(cmdbuf, 0, sizeof(cmdbuf));
reflag = false;//因为可能是调用了re(连续两次login)会导致错误,这里错误要让用户重新输入,否则会崩溃(cmdbuf是空)
cout << prompt << flush;
continue;
}
switch (cmdvalue)
{
case 0:
if (cmdvec.size() != 4)
{
cerr << "error> The number of parameters for [register] is wrong!" << endl;
break;
}
else if (cmdvec[2] != cmdvec[3])
{
cerr << "error> The passwords entered twice are not equal!" << endl;
break;
}
else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
if(strcmp(recvbuf, "success")==0)
cout << "The registration is successful and you can log in!" << endl;
else
cout << "Registration failed, please try a different ID!" << endl;

break;
}

case 1:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [login] is wrong!" << endl;
break;
}

else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的

if(strcmp(recvbuf, "success")==0)
{
cout << "Login successfully!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cout << recvbuf <<endl;
}
break;
}

case 2:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [search] is wrong!" << endl;
break;
}
else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
if(strcmp(recvbuf, "faliure")==0)
{
cout << "sid does not exist!" << endl;
}
else
{
cout << recvbuf <<endl;
}
break;
}

case 3:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [chat] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送

state = clientState::isWaiting;
prompt = promptMap[state];
chattarg = cmdvec[1];
cout << "waiting> Waiting for a response, you can input break or @break to back to command line..." << endl;

//响应在接收线程处理!
/*
Sleep(3000);//阻塞3秒,假装等待连接
state = clientState::isChatting;
//chatSname = ...
prompt = promptMap[state] + chatSname + "> ";
*/
break;
}

case 4:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [accept] is wrong!" << endl;
break;
}
else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
break;
}

case 5:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [reject] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
break;
}

case 6:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [break] is wrong!" << endl;
break;
}
else
{
//做处理,break也需要发送,这样服务器才能知道用户从某请求的等待中退出了
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
state = clientState::cmdLine;
prompt = promptMap[state];
break;
}

case 7:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [send] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
break;
}

case 8:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [sendfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
//响应在接收线程处理!
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 9:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptfile] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
//...还需要用一个线程来接收,文件名通过请求表维护
break;
}

case 10:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectfile] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
break;
}

case 11:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [getfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
//响应在接收线程处理!
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 12:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptget] is wrong!" << endl;
break;
}
else
{
//做处理
//响应在接收线程处理!
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
break;
}

case 13:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectget] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
break;
}

case 14:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsid] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
//需要接收响应,因为sid是全局唯一的,可能设置失败!
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf <<endl;
break;
}

case 15:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsname] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
//这里接收响应是为了增加用户体验,服务器返回一个成功信息
//因为设置相对频率较少,这不是什么很大的负担
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf <<endl;
break;
}

case 16:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [re] is wrong!" << endl;
reflag = false;//虽然是re,但是命令有问题
break;
}
else//即使是错误命令也存,用户可能头铁...就想再试一次
{
//做处理
if (!strcmp(cmdtmp, "no cmd"))//如果还没有命令
{
cerr << "error> No command yet!" << endl;
}
else
{
cout << "reEX> " << cmdtmp;//不用换行,cmdtmp自然有个'\n'
//strcpy已不可用,不指定长度不安全。使用strlen要+1,因为长度不包含结束符,要补上去
strcpy_s(cmdbuf, strlen(cmdtmp) + 1, cmdtmp);
reflag = true;//表明不必把cmdbuf置零
}
break;
}

case 17:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [exit] is wrong!" << endl;
break;
}
else
{
//做处理,或许需要告知服务器退出,先这样
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
exitflag = true;
cout << "The client now exits!" <<endl;
break;
}

case 18:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [hisir] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf <<endl;
break;
}

case 20:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [oyasumi] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()),0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf <<endl;
exitflag = true;
break;
}

}

//是否退出
if(exitflag)
break;

if (cmdvalue == 16 && reflag)//如果是re且成功的话就不用cmd,不用cmdbuf置零
continue;
else if (cmdvalue == 16)//虽然是re但失败了,不能保存re
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
}
else
{
strcpy_s(cmdtmp, strlen(cmdbuf) + 1, cmdbuf);//暂存上一次命令,方便re调用,如果上一次是re的话,不更新
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
reflag = false;//上一次不是re自然置false
}
}


}
}

解析接收命令

进一步丰富接收线程的接收函数,可以借助前面的解析函数parse

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
int fileNumber = 0;//全局变量,用于对方acceptget后选择文件的最大数量
unordered_map<int,string> getResourceMap;
//解析命令,返回解析成不成功,失败则false
bool parseRecv(string cmdrecv)
{
//最后加一个空格,这样能解析成功
cmdrecv += " ";
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = cmdrecv.find(' ', pos)) != string::npos)
{
res.push_back(cmdrecv.substr(pos, pos1 - pos));
while (cmdrecv[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}

if(res.size()<2)
{
cerr << endl <<"An error command was received in the receThread!" <<endl;
return false;
}

if(res[0] == "chat" && state == clientState::isWaiting)//必须还在等待
{
if(res[1] == "accept")
{
cout << endl <<"The peer agrees to chat!" <<endl;
state = clientState::isChatting;
prompt = promptMap[state]+ chattarg+"> ";
}
else if(res[1] == "reject")
{
cout << endl <<"The peer disagrees to chat!" <<endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cerr << endl <<"An error command was received in the receThread!" <<endl;
return false;
}
}
else if(res[0] == "sendfile" && state == clientState::isWaiting)
{
if(res[1] == "accept")
{
cout << endl <<"The peer agrees to receive file!" <<endl;
state = clientState::cmdLine;
prompt = promptMap[state];
//开线程
}
else if(res[1] == "reject")
{
cout << endl <<"The peer disagrees to receive file!" <<endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cerr << endl <<"An error command was received in the receThread!" <<endl;
return false;
}
}
else if(res[0] == "getfile" && state == clientState::isWaiting)
{
if(res[1] == "accept" && res[2] == "none")//如果没有资源但同意了,第三个参数就是none
{
cerr << endl <<"The peer has no resources!" <<endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else if(res[1] == "accept")
{
chooseflag = true;
prompt = "enter the number of file> ";
cout << endl <<"The peer agrees to getfile!" <<endl;
getResourceMap.clear();//清空
fileNumber = 0;//重置
string resolist;//打印信息
//假设对方发送的文件资源表是[1] some.txt [2] some2.txt,即空格分隔的成对的
for(int i=2;i<res.size();i+=2)
{
fileNumber++;
getResourceMap[fileNumber] = res[i+1];
resolist += res[i]+"\t"+res[i+1]+"\n";
}
cout << resolist <<flush;


}
else if(res[1] == "reject")
{
cout << endl <<"The peer doesn't allow you to get the resource!" <<endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cerr << endl <<"An error command was received in the receThread!" <<endl;
return false;
}
}
else if(res[0] == "choosefile")
{
int num = stoi(res[1]);
cout << endl <<"The peer requested the ["+res[1]+"] file!" <<endl;
//开一个线程发送
}
else
return false;
return true;
}
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
void recvThread(const char* SERVER_IP, int MYPORT)
{
//连接服务器
SOCKET connfd = connect_S(SERVER_IP, MYPORT);//获取套接字
if (connfd == INVALID_SOCKET || connfd == SOCKET_ERROR)
exit(1);//连接失败退出,错误由connect_S函数输出

//--------------------------工作-----------------------------
char recvbuf[1024];
while (1)//后面会设置exitflag
{
memset(recvbuf, 0, sizeof(recvbuf));//每次都将buffer清空,防止被上次写入的结果影响
size_t recvbytes = recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
//异常结束时,退出
if (recvbytes <= 0)
{
cout << endl << "The receiving thread exits!" << endl << prompt << flush;
break;
}

if(recvbytes >= 3 && recvbuf[0]=='@' && recvbuf[1]=='#')//这种情况下要解析命令
{
string cmdrecv = recvbuf;
if(parseRecv(cmdrecv.substr(2,cmdrecv.size()-2)))
cout << prompt << flush;//失败就不打印刷新
}
else//其他信息直接输出
{
cout << endl << recvbuf << endl;
cout << prompt << flush;
}

}
//退出
close_S(connfd);
}

测试

需要两个服务器来模拟输入输出,代码不贴了(感觉没人能follow到这…写着写着发现太抽象了)。

不过测试发现挺好的,测了好久关了忘记截图了,不想再测了,因为手敲服务器的响应有点麻烦。

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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
//test
#include<iostream>
#include<cstring>//string.h
#include<string>
#include<unordered_map>

#include <thread>
#include<functional>
#include<WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton
#include <windows.h>//Sleep()

#pragma comment(lib,"ws2_32.lib")//链接dll
using namespace std;

string chatSname = "";
int fileNumber = 0;//全局变量,用于对方acceptget后选择文件的最大数量
unordered_map<int, string> getResourceMap;
const unordered_map<string, int> cmdmap =
{
{"register",0},
{"login",1},
{"search",2},
{"chat",3},
{"accept",4},
{"reject",5},
{"break",6},
{"send",7},
{"sendfile",8},
{"acceptfile",9},
{"rejectfile",10},
{"getfile",11},
{"acceptget",12},
{"rejectget",13},
{"setsid",14},
{"setsname",15},
{"re",16},
{"exit",17},
{"hisir",18},
{"oyasumi",20}
};


enum class clientState
{
noLogin = 0,//未登录状态
cmdLine,//命令行状态
isChatting,//chat状态
isWaiting//等待状态
};

unordered_map<clientState, string> promptMap =
{
{clientState::noLogin,"need login> "},
{clientState::cmdLine,"cmd> "},
{clientState::isChatting,"chatting with "},//进入chat状态要根据对方的sname来拼接
{clientState::isWaiting,"waiting> "}
};


//去掉string首尾空格
void trim(string& s)
{
if (!s.empty())
{
s.erase(0, s.find_first_not_of(" \t"));//首次出现不匹配空格的位置
s.erase(s.find_last_not_of(" \t") + 1);
}
}

//解析命令,cmdstr首位空格都去掉了
vector<string> parse(string cmdstr)
{
//首先看第一个字符是不是@,是的话去掉就好了
if (cmdstr[0] == '@')
cmdstr = cmdstr.substr(1, cmdstr.size() - 1);

//对于一个关键字的命令,无法用空格分割,考虑到最后一定有个\n是没用的,因此把\n改为空格,一举两得
cmdstr[cmdstr.size() - 1] = ' ';
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = cmdstr.find(' ', pos)) != string::npos)
{
res.push_back(cmdstr.substr(pos, pos1 - pos));
while (cmdstr[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}

return res;

}
clientState state = clientState::noLogin;
string prompt = promptMap[state];
string chattarg = "";
bool chooseflag = false;
bool checkCmd(int cmdvalue)
{
//最开始只考虑是否登录
if (cmdvalue == 0 || cmdvalue == 1)
{
if (state != clientState::noLogin)
{
cerr << "error> Have signed in yet!" << endl;
return false;
}
}
else
{
if (state == clientState::noLogin)
{
cerr << "error> Not signed in yet!" << endl;
return false;
}
}
//如果正在chatting,不允许再chat
if (state == clientState::isChatting && cmdvalue == 3)
{
cerr << "error> You are chatting with " << chatSname << "! You can break to chat with other." << endl;
return false;
}

//如果正在等待,只允许break
if (state == clientState::isWaiting && cmdvalue != 6)
{
cerr << "error> Only break or @break can input because you are waiting for something!" << endl;
return false;
}

return true;
}



bool isNumber(const char* buf)//判断字符传是否为数字
{
for (int i = 0; buf[i] != '\n'; i++)//以\n结尾
if (buf[i] < '0' || buf[i]>'9')//每个字符都要是数字
return false;
return true;
}

void run(SOCKET connfd)
{
//一个汉字两字节,可能需要更大的buf
char cmdbuf[256];
char cmdtmp[256] = "no cmd";
char recvbuf[256];
bool reflag = false;
bool exitflag = false;//函数退出信号
cout << prompt << flush;
//根据bool或运算顺序,如果用上一次的结果(reflag==true),就不接收字符
while (reflag || fgets(cmdbuf, sizeof(cmdbuf), stdin) != NULL)//gets已不被编译器支持,不太安全
{
if (chooseflag)//如果在选择文件。接收线程会把这个flag改成true,并且打印提示符enter file number>
{
if (isNumber(cmdbuf))
{
//要进行数字的范围判断,用stoi
int num = stoi(cmdbuf);
if (num > fileNumber || num < 1)
{
cerr << "Please enter a number in the range!" << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}
send(connfd, cmdbuf, sizeof(cmdbuf), 0);//发送
memset(cmdbuf, 0, sizeof(cmdbuf));
//...还需要用一个线程来接收

chooseflag = false;
state = clientState::cmdLine;
prompt = promptMap[state];
cout << prompt << flush;
continue;
}

else
{
cerr << "Please enter a number!" << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}
}
if (state == clientState::isChatting && cmdbuf[0] != '@')//如果在chatting,并且输入第一个不是@,判定为聊天内容直接发送
{
//send...
send(connfd, cmdbuf, sizeof(cmdbuf), 0);//发送
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
}
else//剩下的内容都是else的
{
memset(recvbuf, 0, sizeof(recvbuf));//把接收缓冲清零
string cmdstr(cmdbuf);
trim(cmdstr);

if (cmdstr == "\n")//不管怎么样都有个换行,仅有一个换行就不管
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}
vector<string> cmdvec = parse(cmdstr);

//查找命令
auto iter = cmdmap.find(cmdvec[0]);
if (iter == cmdmap.end())//命令错误
{
cerr << "Wrong command, your command parsed is: " << cmdvec[0] << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}

int cmdvalue = iter->second;
if (!checkCmd(cmdvalue))//如果命令与状态不匹配
{
memset(cmdbuf, 0, sizeof(cmdbuf));
reflag = false;//因为可能是调用了re(连续两次login)会导致错误,这里错误要让用户重新输入,否则会崩溃(cmdbuf是空)
cout << prompt << flush;
continue;
}
switch (cmdvalue)
{
case 0:
if (cmdvec.size() != 4)
{
cerr << "error> The number of parameters for [register] is wrong!" << endl;
break;
}
else if (cmdvec[2] != cmdvec[3])
{
cerr << "error> The passwords entered twice are not equal!" << endl;
break;
}
else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
if (strcmp(recvbuf, "success") == 0)
cout << "The registration is successful and you can log in!" << endl;
else
cout << "Registration failed, please try a different ID!" << endl;

break;
}

case 1:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [login] is wrong!" << endl;
break;
}

else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的

if (strcmp(recvbuf, "success") == 0)
{
cout << "Login successfully!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cout << recvbuf << endl;
}
break;
}

case 2:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [search] is wrong!" << endl;
break;
}
else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
if (strcmp(recvbuf, "faliure") == 0)
{
cout << "sid does not exist!" << endl;
}
else
{
cout << recvbuf << endl;
}
break;
}

case 3:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [chat] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送

state = clientState::isWaiting;
prompt = promptMap[state];
chattarg = cmdvec[1];
cout << "waiting> Waiting for a response, you can input break or @break to back to command line..." << endl;

//响应在接收线程处理!
/*
Sleep(3000);//阻塞3秒,假装等待连接
state = clientState::isChatting;
//chatSname = ...
prompt = promptMap[state] + chatSname + "> ";
*/
break;
}

case 4:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [accept] is wrong!" << endl;
break;
}
else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
break;
}

case 5:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [reject] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
break;
}

case 6:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [break] is wrong!" << endl;
break;
}
else
{
//做处理,break也需要发送,这样服务器才能知道用户从某请求的等待中退出了
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
state = clientState::cmdLine;
prompt = promptMap[state];
break;
}

case 7:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [send] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
break;
}

case 8:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [sendfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
//响应在接收线程处理!
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 9:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptfile] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
//...还需要用一个线程来接收,文件名通过请求表维护
break;
}

case 10:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectfile] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
break;
}

case 11:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [getfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
//响应在接收线程处理!
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 12:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptget] is wrong!" << endl;
break;
}
else
{
//做处理
//响应在接收线程处理!
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
break;
}

case 13:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectget] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
break;
}

case 14:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsid] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
//需要接收响应,因为sid是全局唯一的,可能设置失败!
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf << endl;
break;
}

case 15:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsname] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
//这里接收响应是为了增加用户体验,服务器返回一个成功信息
//因为设置相对频率较少,这不是什么很大的负担
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf << endl;
break;
}

case 16:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [re] is wrong!" << endl;
reflag = false;//虽然是re,但是命令有问题
break;
}
else//即使是错误命令也存,用户可能头铁...就想再试一次
{
//做处理
if (!strcmp(cmdtmp, "no cmd"))//如果还没有命令
{
cerr << "error> No command yet!" << endl;
}
else
{
cout << "reEX> " << cmdtmp;//不用换行,cmdtmp自然有个'\n'
//strcpy已不可用,不指定长度不安全。使用strlen要+1,因为长度不包含结束符,要补上去
strcpy_s(cmdbuf, strlen(cmdtmp) + 1, cmdtmp);
reflag = true;//表明不必把cmdbuf置零
}
break;
}

case 17:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [exit] is wrong!" << endl;
break;
}
else
{
//做处理,或许需要告知服务器退出,先这样
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
exitflag = true;
cout << "The client now exits!" << endl;
break;
}

case 18:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [hisir] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf << endl;
break;
}

case 20:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [oyasumi] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf << endl;
exitflag = true;
break;
}

}

//是否退出
if (exitflag)
break;

if (cmdvalue == 16 && reflag)//如果是re且成功的话就不用cmd,不用cmdbuf置零
continue;
else if (cmdvalue == 16)//虽然是re但失败了,不能保存re
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
}
else
{
strcpy_s(cmdtmp, strlen(cmdbuf) + 1, cmdbuf);//暂存上一次命令,方便re调用,如果上一次是re的话,不更新
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
reflag = false;//上一次不是re自然置false
}
}


}
}

SOCKET connect_S(const char* SERVER_IP, int MYPORT)//返回连接句柄
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
return INVALID_SOCKET;
}

//创建套接字
SOCKET connfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (connfd == INVALID_SOCKET)
{
std::cerr << "socket error !" << std::endl;
return INVALID_SOCKET;
}

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(MYPORT);//服务器端口,自己连接后的端口是os分配的,由进程选择一个端口去连服务器
//socketaddr.sin_addr.s_addr = inet_addr(SERVER_IP); ///服务器ip
//inet_addr最好换成inet_aton(),不会冤枉0.0.0.0和255.255.255.255
struct in_addr inaddr;
inet_pton(AF_INET, SERVER_IP, &inaddr);//windows下相当于inet_aton的函数,多了第一个参数表明是ipv4还是ipv6
socketaddr.sin_addr = inaddr;

///连接服务器,成功返回0,错误返回-1。返回的描述符connfd,该socket包含了服务器ip、port,自己ip、port,可用于发送和接收数据
if (connect(connfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == SOCKET_ERROR)
{
std::cerr << "connect fail !" << std::endl;
return SOCKET_ERROR;
}
return connfd;
}
/*
注意该函数还没有
closesocket(connfd);和WSACleanup();
我们写一个关闭函数
*/
//调用多少次connect_S,就要调用多少次close_S
void close_S(SOCKET connfd)
{
closesocket(connfd);
WSACleanup();
}

//解析命令,返回解析成不成功,失败则false
bool parseRecv(string cmdrecv)
{
//最后加一个空格,这样能解析成功
cmdrecv += " ";
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = cmdrecv.find(' ', pos)) != string::npos)
{
res.push_back(cmdrecv.substr(pos, pos1 - pos));
while (cmdrecv[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}

if (res.size() < 2)
{
cerr << endl << "An error command was received in the receThread!" << endl;
return false;
}

if (res[0] == "chat" && state == clientState::isWaiting)//必须还在等待
{
if (res[1] == "accept")
{
cout << endl << "The peer agrees to chat!" << endl;
state = clientState::isChatting;
prompt = promptMap[state] + chattarg + "> ";
}
else if (res[1] == "reject")
{
cout << endl << "The peer disagrees to chat!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cerr << endl << "An error command was received in the receThread!" << endl;
return false;
}
}
else if (res[0] == "sendfile" && state == clientState::isWaiting)
{
if (res[1] == "accept")
{
cout << endl << "The peer agrees to receive file!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
//开线程
}
else if (res[1] == "reject")
{
cout << endl << "The peer disagrees to receive file!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cerr << endl << "An error command was received in the receThread!" << endl;
return false;
}
}
else if (res[0] == "getfile" && state == clientState::isWaiting)
{
if (res[1] == "accept" && res[2] == "none")//如果没有资源但同意了,第三个参数就是none
{
cerr << endl << "The peer has no resources!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else if (res[1] == "accept")
{
chooseflag = true;
prompt = "enter the number of file> ";
cout << endl << "The peer agrees to getfile!" << endl;
getResourceMap.clear();//清空
fileNumber = 0;//重置
string resolist;//打印信息
//假设对方发送的文件资源表是[1] some.txt [2] some2.txt,即空格分隔的成对的
for (int i = 2; i < res.size(); i += 2)
{
fileNumber++;
getResourceMap[fileNumber] = res[i + 1];
resolist += res[i] + "\t" + res[i + 1] + "\n";
}
cout << resolist << flush;


}
else if (res[1] == "reject")
{
cout << endl << "The peer doesn't allow you to get the resource!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cerr << endl << "An error command was received in the receThread!" << endl;
return false;
}
}
else if (res[0] == "choosefile")
{
int num = stoi(res[1]);
cout << endl << "The peer requested the [" + res[1] + "] file!" << endl;
//开一个线程发送
}
else
return false;
return true;
}

void recvThread(const char* SERVER_IP, int MYPORT)
{
//连接服务器
SOCKET connfd = connect_S(SERVER_IP, MYPORT);//获取套接字
if (connfd == INVALID_SOCKET || connfd == SOCKET_ERROR)
exit(1);//连接失败退出,错误由connect_S函数输出

//--------------------------工作-----------------------------
char recvbuf[1024];
while (1)//后面会设置exitflag
{
memset(recvbuf, 0, sizeof(recvbuf));//每次都将buffer清空,防止被上次写入的结果影响
size_t recvbytes = recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
//异常结束时,退出
if (recvbytes <= 0)
{
cout << endl << "The receiving thread exits!" << endl << prompt << flush;
break;
}

if (recvbytes >= 3 && recvbuf[0] == '@' && recvbuf[1] == '#')//这种情况下要解析命令
{
string cmdrecv = recvbuf;
if (parseRecv(cmdrecv.substr(2, cmdrecv.size() - 2)))
cout << prompt << flush;//失败就不打印刷新
}
else//其他信息直接输出
{
cout << endl << recvbuf << endl;
cout << prompt << flush;
}



}
//退出
close_S(connfd);
}


const char* SERVER_IP = "192.168.248.131";
int MYPORT = 8000;
int MYPORT2 = 9000;
int main()
{
thread(bind(recvThread, SERVER_IP, MYPORT)).detach();
//连接服务器
SOCKET connfd = connect_S(SERVER_IP, MYPORT2);//获取套接字
run(connfd);
//退出
close_S(connfd);
return 0;
}

请求表设计

前面提到,我们总要考虑用户“无故”输入“无效”命令的情况(往最坏的情况上考虑),当用户没有被请求时,用户可能输入accept和reject这类命令,此时是无效的。为了快速响应以及降低服务器的复杂度,我们将这类无效命令在本地判断,因此需要一个请求表维护到来的请求(chat、sendfile、getfile)。

  • 当接收线程接收到一个请求时,继续解析以获得sid。比如对方发送一个chat sid时,服务器的命令接收端会得到请求方的sid等信息,然后把命令发给该sid的接收线程,格式像这样:@#chatfrom [sid] [sname]。只需要把该sid获取然后添加到请求表内即可。对于getfile:@#getfilefrom [sid] [sname];对于sendfile:@#sendfilefrom [sid] [sname] [filename]
  • 当用户accept时,查表,只有存在才允许把该命令发送到服务器,然后服务器会发一个@#chat accept给对方,就完成了。reject同理。并且accept和reject后,都要把请求表内对应的表项删除。
  • 当对方请求到来时,本地会更新请求表添加表项;然而对方用户可能在等待状态break,这时要通知本地对方取消请求并删除请求表项。但是在前面我们对用户的break命令仅仅只是发送了一个break命令,我们还需要做更多的处理。
    • 我们选择在服务器端处理,服务器是可以得知用户在什么状态的。在noLogin状态不允许break;cmdLine状态下,break是无效的。服务器真正要区分的是用户在waiting还是在chatting。因此,当用户在cmdLine调用break时,不做处理。
    • 当用户发送chat、sendfile、getfile后,服务器在转发信息时标记用户进入waiting状态。注意我们的设计,在waiting时只能break或等待,因此用户在同一时间最多只允许有一个请求。一旦用户在waiting时发送一个break,服务器在接收后会把用户标记回cmdLine状态,并且服务器还需要知道这个break发回给谁,因此服务器在标记用户进入wating时还必须添加用户之间的映射,通过这个映射把@#break [sid]发过去,并随后把这个映射删除。一旦对方接收到这个回复,就从请求表中把该sid对应的表项删除。
    • 如果accept了一个chatting请求,服务器会标记双方进入chatting状态。此时如果某一方break,服务器通过映射找到另一方,发一个@#break now,对方接收后从chatting状态退出即可。
    • 我们再考虑两种情况:
      • 当用户在waiting状态下break,服务器标记用户回到cmdLine,而对方还没有接收到这个消息就accept了,由于用户已经回到cmdLine状态了,这个accept是失效的,因此服务器通过cmdLine状态得知这一事件,向accept一方发回一个@#break now,让其退回命令行状态(如果是chatting的话)并打印”the peers break before you accept”。如果是reject就不管
      • 当用户在waiting状态下break,服务器还没标记用户回到cmdLine,对方就发了accept,此时由于服务器发现用户还在waiting,就把accept转过去了,但用户已经回到命令行了。在接收线程里,如果用户不在等待状态则accept是无效的,因此只用考虑向accept发送处理。服务器把accept转过去后,标记的用户的状态要么返回cmdLine(sendfile这些被accept就标记为cmdLine),要么进入chatting状态。然后服务器又收到break信息,前面说到在chatting状态收到break,服务器发一个@#break now。则如果accept方目前在chatting,也可以退出;否则用户应该在cmdLine状态,这样收到@#break now,给accept方打印一个信息”the peers break before you accept”。
  • 因为用户最多一个请求,则**三个请求命令可以共用一个请求表,映射如下:[sid] [cmd],如 123->”sendfile”**。

至此,我们知道了请求表的设计和接收线程该如何修改。


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
//请求表封装
#include <iostream>
#include <string>
#include <unordered_map>
#include <mutex>

using namespace std;


//对于chat和getfile命令,只存储sid和命令关键字,对于sendfile,还需要存储文件的名称,
//如sendfilesome.txt,因为sendfile是8个字节,所以很容易解析文件名

class ReqTable//主线程和接收线程都需要互斥操作
{
private:
unordered_map<string, string> reqTable;
using iterator = unordered_map<string, string>::iterator;
string filename;
mutex reqTableMux;

public:
ReqTable(){}
~ReqTable(){}
void insertReq(string sid,string cmd);//插入表项
void deleteReq(string sid);//删除表项
string checkReq(string sid);//查找并删除,如果是sendfile,让用户调用getName函数获取
string getName() { return filename; }

void print()//打印表项,后续或许可以加一个打印请求表的命令,现在用作测试
{
for (auto ite = reqTable.begin(); ite != reqTable.end(); ite++)
cout << ite->first << "\t" << ite->second << endl;
}

};

void ReqTable::insertReq(string sid, string cmd)
{
lock_guard<mutex> locker(reqTableMux);
reqTable[sid] = cmd;
}

void ReqTable::deleteReq(string sid)
{
lock_guard<mutex> locker(reqTableMux);
iterator iter = reqTable.find(sid);

if (iter == reqTable.end())//不存在则直接不管
return;
else
reqTable.erase(iter);

}

string ReqTable::checkReq(string sid)
{
lock_guard<mutex> locker(reqTableMux);
iterator iter = reqTable.find(sid);

if (iter == reqTable.end())//不存在则返回空,交由上层处理
return "";
else
{
string cmd = iter->second;
if (cmd.size() >= 8)//这种情况是sendfile,设置filename
{
filename = cmd.substr(8);
cmd = "sendfile";
}
//查找完不能删除,还不能删除,因为可能accept不对应,要保留
//reqTable.erase(iter);
return cmd;
}
}
//测试
int main()
{
ReqTable reqTable;
reqTable.insertReq("123", "chat");
reqTable.insertReq("456", "sendfilea.txt");
reqTable.insertReq("789", "getfile");
reqTable.print();

cout << reqTable.checkReq("789") << endl;

//判断后获取文件
if (reqTable.checkReq("456") == "sendfile")
cout << reqTable.getName() << endl;

reqTable.deleteReq("123");
cout << reqTable.checkReq("123") << endl;
reqTable.deleteReq("123");
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
ReqTable reqTable;

//解析命令,返回解析成不成功,失败则false
bool parseRecv(string cmdrecv)
{
//最后加一个空格,这样能解析成功
cmdrecv += " ";
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = cmdrecv.find(' ', pos)) != string::npos)
{
res.push_back(cmdrecv.substr(pos, pos1 - pos));
while (cmdrecv[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}

if (res.size() < 2)
{
cerr << endl << "An error command was received in the receThread!" << endl;
return false;
}

if (res[0] == "chat" && state == clientState::isWaiting)//必须还在等待
{
if (res[1] == "accept")
{
cout << endl << "The peer agrees to chat!" << endl;
state = clientState::isChatting;
prompt = promptMap[state] + chattarg + "> ";
}
else if (res[1] == "reject")
{
cout << endl << "The peer disagrees to chat!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cerr << endl << "An error command was received in the receThread!" << endl;
return false;
}
}
else if (res[0] == "sendfile" && state == clientState::isWaiting)
{
if (res[1] == "accept")
{
cout << endl << "The peer agrees to receive file!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
//开线程发送
}
else if (res[1] == "reject")
{
cout << endl << "The peer disagrees to receive file!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cerr << endl << "An error command was received in the receThread!" << endl;
return false;
}
}
else if (res[0] == "getfile" && state == clientState::isWaiting)
{
if (res[1] == "accept" && res[2] == "none")//如果没有资源但同意了,第三个参数就是none
{
cerr << endl << "The peer has no resources!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else if (res[1] == "accept")
{
chooseflag = true;
prompt = "enter the number of file> ";
cout << endl << "The peer agrees to getfile!" << endl;
getResourceMap.clear();//清空
fileNumber = 0;//重置
string resolist;//打印信息
//假设对方发送的文件资源表是[1] some.txt [2] some2.txt,即空格分隔的成对的
for (int i = 2; i < res.size(); i += 2)
{
fileNumber++;
getResourceMap[fileNumber] = res[i + 1];
resolist += res[i] + "\t" + res[i + 1] + "\n";
}
cout << resolist << flush;


}
else if (res[1] == "reject")
{
cout << endl << "The peer doesn't allow you to get the resource!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}

else
{
cerr << endl << "An error command was received in the receThread!" << endl;
return false;
}
}
else if (res[0] == "choosefile")
{
int num = stoi(res[1]);
cout << endl << "The peer requested the [" + res[1] + "] file!" << endl;
//开一个线程发送
}
//---------------------------------------------------------------------------------
//-----------------------------请求表相关命令----------------------------------------
else if (res[0] == "chatfrom")
{
reqTable.insertReq(res[1], "chat");
cout << endl << "Receive a chat request from " << res[2] <<" -sid:" << res[1] << endl;
}
else if (res[0] == "sendfilefrom")
{
string req = "sendfile"+res[3];
reqTable.insertReq(res[1],req);
cout << endl << "Receive a sendfile request from " << res[2] <<" -sid: " << res[1] <<" -file name: " << res[3] << endl;
}
else if (res[0] == "getfilefrom")
{
reqTable.insertReq(res[1], "getfile");
cout << endl << "Receive a getfile request from " << res[2] <<" -sid:" << res[1] << endl;
}
//----break-----
else if (res[0] == "break")
{
if(res[1] == "now")
{
if(state == clientState::isChatting)//进入虚假chatting,让其退出
{
cout << endl << "The peers break before you accept!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else//进入接收或发送
{
cout << endl << "The peers break before you accept!" << endl;
//要让接收或发送线程退出
}
}

else//这种情况是break [sid],用户sid撤回请求
reqTable.deleteReq(res[1]);
}
else
return false;
return true;
}

现在对accept那些命令进行更改,即修改run函数,这里把整个run都弄下来,分开弄到时候合在一起也麻烦,这里还改一下choosefile的发送

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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
void run(SOCKET connfd)
{
//一个汉字两字节,可能需要更大的buf
char cmdbuf[256];
char cmdtmp[256] = "no cmd";
char recvbuf[256];
bool reflag = false;
bool exitflag = false;//函数退出信号
cout << prompt << flush;
//根据bool或运算顺序,如果用上一次的结果(reflag==true),就不接收字符
while (reflag || fgets(cmdbuf, sizeof(cmdbuf), stdin) != NULL)//gets已不被编译器支持,不太安全
{
if (chooseflag)//如果在选择文件。接收线程会把这个flag改成true,并且打印提示符enter file number>
{
if (isNumber(cmdbuf))
{
//要进行数字的范围判断,用stoi
int num = stoi(cmdbuf);
if (num > fileNumber || num < 1)
{
cerr << "Please enter a number in the range!" << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}
string choosecmd = "choosefile " + string(cmdbuf);//拼接命令
send(connfd, choosecmd.c_str(), choosecmd.size(), 0);//发送
memset(cmdbuf, 0, sizeof(cmdbuf));
//...还需要用一个线程来接收

chooseflag = false;
state = clientState::cmdLine;
prompt = promptMap[state];
cout << prompt << flush;
continue;
}

else
{
cerr << "Please enter a number!" << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}
}
if (state == clientState::isChatting && cmdbuf[0] != '@')//如果在chatting,并且输入第一个不是@,判定为聊天内容直接发送
{
//send...
send(connfd, cmdbuf, sizeof(cmdbuf), 0);//发送
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
}
else//剩下的内容都是else的
{
memset(recvbuf, 0, sizeof(recvbuf));//把接收缓冲清零
string cmdstr(cmdbuf);
trim(cmdstr);

if (cmdstr == "\n")//不管怎么样都有个换行,仅有一个换行就不管
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}
vector<string> cmdvec = parse(cmdstr);

//查找命令
auto iter = cmdmap.find(cmdvec[0]);
if (iter == cmdmap.end())//命令错误
{
cerr << "Wrong command, your command parsed is: " << cmdvec[0] << endl;
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
continue;
}

int cmdvalue = iter->second;
if (!checkCmd(cmdvalue))//如果命令与状态不匹配
{
memset(cmdbuf, 0, sizeof(cmdbuf));
reflag = false;//因为可能是调用了re(连续两次login)会导致错误,这里错误要让用户重新输入,否则会崩溃(cmdbuf是空)
cout << prompt << flush;
continue;
}
switch (cmdvalue)
{
case 0:
if (cmdvec.size() != 4)
{
cerr << "error> The number of parameters for [register] is wrong!" << endl;
break;
}
else if (cmdvec[2] != cmdvec[3])
{
cerr << "error> The passwords entered twice are not equal!" << endl;
break;
}
else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
if (strcmp(recvbuf, "success") == 0)
cout << "The registration is successful and you can log in!" << endl;
else
cout << "Registration failed, please try a different ID!" << endl;

break;
}

case 1:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [login] is wrong!" << endl;
break;
}

else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的

if (strcmp(recvbuf, "success") == 0)
{
cout << "Login successfully!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cout << recvbuf << endl;
}
break;
}

case 2:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [search] is wrong!" << endl;
break;
}
else
{
//做处理
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
if (strcmp(recvbuf, "faliure") == 0)
{
cout << "sid does not exist!" << endl;
}
else
{
cout << recvbuf << endl;
}
break;
}

case 3:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [chat] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
//发送cmdstr就不用做头尾空格去除
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送

state = clientState::isWaiting;
prompt = promptMap[state];
chattarg = cmdvec[1];
cout << "waiting> Waiting for a response, you can input break or @break to back to command line..." << endl;

//响应在接收线程处理!
/*
Sleep(3000);//阻塞3秒,假装等待连接
state = clientState::isChatting;
//chatSname = ...
prompt = promptMap[state] + chatSname + "> ";
*/
break;
}

case 4:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [accept] is wrong!" << endl;
break;
}
else
{
//做处理
//发送cmdstr就不用做头尾空格去除
if(reqTable.checkReq(cmdvec[1]) == "chat")
{
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
reqTable.deleteReq(cmdvec[1]);
}
else
cerr << "error> "<<cmdvec[1]<<" did not send a chat request!" << endl;
break;
}

case 5:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [reject] is wrong!" << endl;
break;
}
else
{
//做处理
if(reqTable.checkReq(cmdvec[1]) == "chat")
{
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
reqTable.deleteReq(cmdvec[1]);
}
else
cerr << "error> "<<cmdvec[1]<<" did not send a chat request!" << endl;
break;
}

case 6:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [break] is wrong!" << endl;
break;
}
else
{
//做处理,break也需要发送,这样服务器才能知道用户从某请求的等待中退出了
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
state = clientState::cmdLine;
prompt = promptMap[state];
break;
}

case 7:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [send] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
break;
}

case 8:
if (cmdvec.size() != 3)
{
cerr << "error> The number of parameters for [sendfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
//响应在接收线程处理!
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 9:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptfile] is wrong!" << endl;
break;
}
else
{
//做处理
if(reqTable.checkReq(cmdvec[1]) == "sendfile")
{
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
reqTable.deleteReq(cmdvec[1]);
string filename = reqTable.getName();
//...还需要用一个线程来接收,文件名通过请求表维护
}
else
cerr << "error> "<<cmdvec[1]<<" did not send a sendfile request!" << endl;


break;
}

case 10:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectfile] is wrong!" << endl;
break;
}
else
{
//做处理
if(reqTable.checkReq(cmdvec[1]) == "sendfile")
{
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
reqTable.deleteReq(cmdvec[1]);
}
else
cerr << "error> "<<cmdvec[1]<<" did not send a sendfile request!" << endl;
break;
}

case 11:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [getfile] is wrong!" << endl;
break;
}
else
{
//做处理
//send...waiting
//响应在接收线程处理!
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
state = clientState::isWaiting;
prompt = promptMap[state];
cout << "waiting> Waiting for a response, you can input @break to back to command line..." << endl;
break;
}

case 12:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [acceptget] is wrong!" << endl;
break;
}
else
{
//做处理
//响应在接收线程处理!
if(reqTable.checkReq(cmdvec[1]) == "getfile")
{
//还要发资源列表
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
reqTable.deleteReq(cmdvec[1]);
}
else
cerr << "error> "<<cmdvec[1]<<" did not send a getfile request!" << endl;
break;
}

case 13:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [rejectget] is wrong!" << endl;
break;
}
else
{
//做处理
if(reqTable.checkReq(cmdvec[1]) == "getfile")
{
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
reqTable.deleteReq(cmdvec[1]);
}
else
cerr << "error> "<<cmdvec[1]<<" did not send a getfile request!" << endl;
break;
}

case 14:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsid] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
//需要接收响应,因为sid是全局唯一的,可能设置失败!
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf << endl;
break;
}

case 15:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [setsname] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
//这里接收响应是为了增加用户体验,服务器返回一个成功信息
//因为设置相对频率较少,这不是什么很大的负担
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf << endl;
break;
}

case 16:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [re] is wrong!" << endl;
reflag = false;//虽然是re,但是命令有问题
break;
}
else//即使是错误命令也存,用户可能头铁...就想再试一次
{
//做处理
if (!strcmp(cmdtmp, "no cmd"))//如果还没有命令
{
cerr << "error> No command yet!" << endl;
}
else
{
cout << "reEX> " << cmdtmp;//不用换行,cmdtmp自然有个'\n'
//strcpy已不可用,不指定长度不安全。使用strlen要+1,因为长度不包含结束符,要补上去
strcpy_s(cmdbuf, strlen(cmdtmp) + 1, cmdtmp);
reflag = true;//表明不必把cmdbuf置零
}
break;
}

case 17:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [exit] is wrong!" << endl;
break;
}
else
{
//做处理,或许需要告知服务器退出,先这样
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
exitflag = true;
cout << "The client now exits!" << endl;
break;
}

case 18:
if (cmdvec.size() != 2)
{
cerr << "error> The number of parameters for [hisir] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf << endl;
break;
}

case 20:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [oyasumi] is wrong!" << endl;
break;
}
else
{
//做处理
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的
cout << recvbuf << endl;
exitflag = true;
break;
}

}

//是否退出
if (exitflag)
break;

if (cmdvalue == 16 && reflag)//如果是re且成功的话就不用cmd,不用cmdbuf置零
continue;
else if (cmdvalue == 16)//虽然是re但失败了,不能保存re
{
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
}
else
{
strcpy_s(cmdtmp, strlen(cmdbuf) + 1, cmdbuf);//暂存上一次命令,方便re调用,如果上一次是re的话,不更新
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
reflag = false;//上一次不是re自然置false
}
}


}
}

到这就改完了,先不测试了,稍后就封装起来,代码量上来了有点难受。但还是等基本完成后再一并把封装好的代码放上来。

资源列表生成

前面提到的资源列表字符串的格式,这里最后有没有空格都没关系:

1
//假设对方发送的文件资源表是[1] some.txt [2] some2.txt,即空格分隔的成对的

我们允许用户指定目录,也允许使用默认的目录,具体的:

  • 在本地目录有个配置文件config.txt,里面允许配置resource_path[空格]路径(这是新增内容)
  • 如果没有找到这个文件,就使用默认路径./resource;如果找到了就更换为该路径
  • 然后找到该文件夹,如果没有则资源列表为空;否则遍历文件,生成资源列表,既生成map也生成能直接发送的字符串。

首先有个读配置的函数,以及判断是否存在文件夹的函数;然后用windows.h的api去遍历文件名。

1
2
3
4
5
6
7
8
9
10
11
HANDLE FindFirstFile(LPCTSTR lpFileName,LPWIN32_FIND_DATA lpFindFileData);
//LPWIN32_FIND_DATA描述文件属性,如只读只写,这里获取名字
//lpFileName为/结尾的路径加通配符,比如这里遍历所有文件,就在路径后面添加了*,代表任意文件
//如果调用成功返回一个句柄,可用来做为FindNextFile或FindClose参数

bool FindNextFileA(HANDLE hFindFile,LPWIN32_FIND_DATAA lpFindFileData);
//继续对 FindFirstFile、FindFirstFileEx 或 FindFirstFileTransacted 函数的上一次调用中的文件搜索。

FindFirstFileA的返回值为HANDLE,即句柄,可与宏INVALID_HANDLE_VALUE进行比较,相等则说明失败,这个宏实际上就是-1
这里因为已经判断了文件夹是否存在,不用比较
FindNextFileA的返回值为true则匹配成功,为false则匹配失败,直接退出循环

函数和测试代码如下:

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
#include <string>//getline
#include <fstream>//文件流
#include <iostream>
#include <unordered_map>//哈希map
#include <windows.h>

using namespace std;
//假设有很多配置,这里生成一个配置映射
unordered_map<string, string> config_map;//映射表
unordered_map<int, string> myresource_map;
string myresource_str;
void setConfig()
{
const char* configfile = "./config.txt";
ifstream cfgFile(configfile, ios::in);

string line;
if (cfgFile.is_open())//如果可以打开,则获取参数
{
while (getline(cfgFile, line)) //逐行读取,直到结束
{
int pos = line.find(' ', 0);//找到空格位置
string key = line.substr(0, pos);//获取子串
string value = line.substr(pos + 1, line.size() - pos - 1);

if (value.size() != 0)//如果确实配置了
{
config_map[key] = value;//添加映射表项
//输出配置
cout << "configuration of [" << key << "]" << " is " << value << endl;
}
}
if (config_map.find("resource_path") == config_map.end())
{
config_map["resource_path"] = "./resource";
cout << "default configuration of [resource_path]" << " is " << "./resource" << endl;
}
cfgFile.close();
}
//没打开就默认
else
{
config_map["resource_path"] = "./resource";
cout << "default configuration of [resource_path]" << " is " << "./resource" << endl;
}
}

//判断文件夹是否存在
//DWORD d = GetFileAttributesA(const char* filename); #include <windows.h> 为windows系统函数,判断文件目录是否存在
bool dirExists(const std::string& dirpath)
{
DWORD ftyp = GetFileAttributesA(dirpath.c_str());
if (ftyp == INVALID_FILE_ATTRIBUTES)
return false; //something is wrong with your path!

if (ftyp & FILE_ATTRIBUTE_DIRECTORY)
return true; // this is a directory!

return false; // this is not a directory!
}

//传入config_map["resource_path"],完整文件路径为config_map["resource_path"]+'/'+myresoure_map[i]
//设置发送字符串myresource_str 以及映射表myresoure_map
void genResource(string dirpath)
{

if (!dirExists(dirpath))
{
myresource_str = "";//没有文件夹
return;
}
WIN32_FIND_DATAA fileInfo;
dirpath = dirpath + "/*";//通配符匹配所有的文件

//通常,最初的两次搜索得到的文件名为:"." 、"..",分别代表当前目录和上级目录,这里过滤掉"."然后进while过滤".."
HANDLE hFile = FindFirstFileA(dirpath.c_str(), &fileInfo);//现在定位到"."

if (hFile == INVALID_HANDLE_VALUE) {
myresource_str = "";//出错
return;
}


FindNextFileA(hFile, &fileInfo);//现在定位到".."
int index = 1;
while (FindNextFileA(hFile, &fileInfo))//再把".."过滤掉
{
myresource_str += "[" + to_string(index) + "] " + fileInfo.cFileName + " ";
myresource_map[index++] = fileInfo.cFileName;

}
FindClose(hFile);//关闭
}

int main()
{
setConfig();
genResource(config_map["resource_path"]);
cout << myresource_str << endl;
return 0;
}

文件传输

我们有时候还需要文件的发送和接收,这在另一个线程进行,下面再梳理一下这些场景:

  • 当sendfile后收到一个accept时,准备发送
  • 当使用一个acceptfile时,如果成功则准备接收
  • 当acceptget后收到一个choosefile返回命令时,准备发送
  • 当chooseflag为true时,输入选择的数字或准备接收

我们还没有定义线程的端口(ip服务器是知道的),这需要在命令里发送,我们先明确一下主被动关系:

  • 使用sendfile时,对方可能会拒绝;因此让对方acceptfile时把默认端口发来,对方进入listen,本地去connect发送
  • 使用getfile时,对方可能会拒绝;当对方acceptget时本地进入choose状态(目前还没设置能退出的命令,当如果对方资源列表为空是不会发过来的),choose后对方收到就会发文件过来,所以choose时把默认端口发去,本地进入listen,对方去connect。

因此需要两个端口号,一个用于acceptfile时开启监听接收,一个用于choose后开启监听接收。

因为对应关系明确,所以如果固定了端口就不需要在命令里再发送了,因为对方也知道;当然也允许用户自己选择端口号(通过配置),这里先不实现,给定:

  • sendfile的端口是14242
  • getfile的端口是14243

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
//发送函数,从调用者获得ip和端口和文件名
#include<iostream>
#include<cstring>
#include<WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton
#include <stdio.h>//FILE等操作
#include <chrono>

#define buffSize 102400
const char* filelog = "fileLog.txt";//记录收发文件信息

void sendfile(const char* server_ip, const int port, std::string filename)
{
//连接服务器
SOCKET connfd = connect_S(server_ip, port);//获取套接字
if (connfd == INVALID_SOCKET || connfd == SOCKET_ERROR)
return;//连接失败退出,错误由connect_S函数输出


char sendbuf[buffSize];
memset(sendbuf, 0, sizeof(sendbuf));

//----------------------打开文件------------------------------------------------------
FILE* fp = NULL;
if (fopen_s(&fp, filename.c_str(), "rb") != 0)//要以二进制形式读写,这样兼容文件格式
{
std::cerr << "cannot open file " << filename << std::endl;
return;
}
std::chrono::system_clock::time_point time1 = std::chrono::system_clock::now();
size_t nSend = 0;
size_t totalSend = 0;
while (1)
{
size_t nRead = fread(sendbuf, 1, buffSize, fp);

if (ferror(fp) != 0)
{
std::cerr << "failed to read file " << std::endl;
return;
}

nSend = send(connfd, sendbuf, int(nRead), 0);//发送nRead个字节

if (nSend == SOCKET_ERROR)//网络断开或copy出错
{
std::cerr << "the connection to server has been failed" << std::endl;
return;
}
totalSend += nSend;

if (feof(fp))//读了,发完,再判断是否到达末尾
{
break;
}
}
fclose(fp);
std::chrono::system_clock::time_point time2 = std::chrono::system_clock::now();
int MB = int(totalSend / (1024 * 1024));
int ms = int(std::chrono::duration_cast<std::chrono::milliseconds>(time2 - time1).count());
std::string result = "send [" + filename + "] [" + std::to_string(totalSend) + "] bytes (" + std::to_string(MB) + " MB) cost [" + std::to_string(ms) + "] ms\n";

if (fopen_s(&fp, filelog, "a") != 0)//写result
{
std::cerr << "cannot open file " << filename << std::endl;
return;
}
fwrite(result.c_str(), 1, result.size(), fp);
fclose(fp);

closesocket(connfd);
}


void recvfile(const int port, std::string filename)
{
//定义socketfd,它要绑定监听的网卡地址和端口
SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
std::cerr << "bind error" << std::endl;
return;
//cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲.
//也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。
}

//开始监听
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cerr << "listen error" << std::endl;
return;
}

///客户端套接字
char recvbuf[buffSize];
struct sockaddr_in client_addr;//获取客户的地址和端口号,连接后的不分配新端口
socklen_t len = sizeof(client_addr);//socklen_t 相当于 int,但使用int必须强制转型告知编译器

SOCKET conn = accept(listenfd, (struct sockaddr*)&client_addr, &len);//阻塞,等待连接,成功则创建连接套接字conn描述这个用户
if (conn == -1)
{
std::cerr << "connect" << std::endl;
return;
}
/*
如果队列中没有等待的连接,套接字也没有被标记为Non-blocking,accept()会阻塞调用函数直到连接出现;
如果套接字被标记为Non-blocking,队列中也没有等待的连接,accept()返回错误EAGAIN或EWOULDBLOCK。
*/

//----------------------打开文件------------------------------------------------------
FILE* fp = NULL;
if (fopen_s(&fp, filename.c_str(), "wb") != 0)//要以二进制形式读写,这样兼容文件格式
{
std::cerr << "cannot open file " << filename << std::endl;
return;
}
std::chrono::system_clock::time_point time1 = std::chrono::system_clock::now();

int totalRecv = 0;
while (1)
{
int nRecv = recv(conn, recvbuf, buffSize, 0);
if (nRecv == SOCKET_ERROR)//copy出错
{
std::cerr << "connection to client has been failed" << std::endl;
return;
}
else if (nRecv == 0)//这种情况是对端close了,此时返回0。可能是意外close,也可能是发送完毕了
{
break;
}
totalRecv += nRecv;

size_t nWrite = fwrite(recvbuf, 1, nRecv, fp);//写文件

if (nWrite != nRecv || ferror(fp) != 0)
{
std::cerr << "failed to write file" << std::endl;
return;
}
}

fclose(fp);
std::chrono::system_clock::time_point time2 = std::chrono::system_clock::now();
int MB = totalRecv / (1024 * 1024);
int ms = int(std::chrono::duration_cast<std::chrono::milliseconds>(time2 - time1).count());
std::string result = "recv [" + filename + "] [" + std::to_string(totalRecv) + "] bytes (" + std::to_string(MB) + " MB) cost [" + std::to_string(ms) + "] ms\n";

if (fopen_s(&fp, filelog, "a") != 0)//写result
{
std::cerr << "cannot open file " << filename << std::endl;
return;
}
fwrite(result.c_str(), 1, result.size(), fp);
fclose(fp);

closesocket(conn);
closesocket(listenfd);
}

现在要思考一下线程如何维护,这里主要是设计前面提到的@#break now命令,即本地用户break后没来得及通知对方,对方就accept了。因为我们设计的是让accept方发送,所以实际上接收后break now命令后只需要把acceptfile后产生的listen线程关掉就好了。

因为只设计监听一个端口,因此sendfile和getfile各自在同一时间只能接收一个文件(用线程池不方便管理,并且消耗的资源太多了;如果accept一个连接新开一个线程,对于文件名是不方便传参的,除非用线程池取任务,这些任务绑定好了参数,目前先不支持吧)。

然后这些线程不能用join,否则会在线程切换时阻塞(比如上一个文件下载完下载另一个要join,但其实不知道下载完没,就可能会阻塞),但不用join又无法释放线程资源;所以用deatch,这就需要给主线程提供是否在运行的信息,设置全局变量isRecvingGf和isRecvingSf即可。因为同一时间只有一个线程改写该全局变量,所以不用互斥。

然后,我们还要考虑虚假acceptfile的情况,这时本地会开一个listen,如果不关掉下一个文件接收就无法bind端口,因此收到break now后,就用本地的一个连接去访问,让其accept,然后判断是本地的“127.0.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
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
void sendfile(const char* server_ip, const int port, std::string filename)
{
//连接服务器
SOCKET connfd = connect_S(server_ip, port);//获取套接字
if (connfd == INVALID_SOCKET || connfd == SOCKET_ERROR)
return;//连接失败退出,错误由connect_S函数输出


char sendbuf[buffSize];
memset(sendbuf, 0, sizeof(sendbuf));

//----------------------打开文件------------------------------------------------------
FILE* fp = NULL;
if (fopen_s(&fp, filename.c_str(), "rb") != 0)//要以二进制形式读写,这样兼容文件格式
{
std::cerr << "cannot open file " << filename << std::endl;
return;
}
std::chrono::system_clock::time_point time1 = std::chrono::system_clock::now();
size_t nSend = 0;
size_t totalSend = 0;
while (1)
{
size_t nRead = fread(sendbuf, 1, buffSize, fp);

if (ferror(fp) != 0)
{
std::cerr << "failed to read file " << std::endl;
return;
}

nSend = send(connfd, sendbuf, int(nRead), 0);//发送nRead个字节

if (nSend == SOCKET_ERROR)//网络断开或copy出错
{
std::cerr << "the connection to server has been failed" << std::endl;
return;
}
totalSend += nSend;

if (feof(fp))//读了,发完,再判断是否到达末尾
{
break;
}
}
fclose(fp);
std::chrono::system_clock::time_point time2 = std::chrono::system_clock::now();
int MB = int(totalSend / (1024 * 1024));
int ms = int(std::chrono::duration_cast<std::chrono::milliseconds>(time2 - time1).count());
std::string result = "send [" + filename + "] [" + std::to_string(totalSend) + "] bytes (" + std::to_string(MB) + " MB) cost [" + std::to_string(ms) + "] ms\n";

if (fopen_s(&fp, filelog, "a") != 0)//写result
{
std::cerr << "cannot open file " << filename << std::endl;
return;
}
fwrite(result.c_str(), 1, result.size(), fp);
fclose(fp);

closesocket(connfd);
}


void recvfile(const int port, std::string filename ,int flag)//这个filename是完整路径
{

if (flag)//1是getfile
isRecvingGf = true;
else
isRecvingSf = true;

//定义socketfd,它要绑定监听的网卡地址和端口
SOCKET listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
std::cerr << "bind error" << std::endl;
if (flag)//1是getfile
isRecvingGf = false;
else
isRecvingSf = false;
closesocket(listenfd);
return;
//cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲.
//也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。
}

//开始监听
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cerr << "listen error" << std::endl;
if (flag)//1是getfile
isRecvingGf = false;
else
isRecvingSf = false;
closesocket(listenfd);
return;
}

///客户端套接字
char recvbuf[buffSize];
struct sockaddr_in client_addr;//获取客户的地址和端口号,连接后的不分配新端口
socklen_t len = sizeof(client_addr);//socklen_t 相当于 int,但使用int必须强制转型告知编译器

SOCKET conn = accept(listenfd, (struct sockaddr*)&client_addr, &len);//阻塞,等待连接,成功则创建连接套接字conn描述这个用户
if (conn == -1)
{
std::cerr << "connect error" << std::endl;
if (flag)//1是getfile
isRecvingGf = false;
else
isRecvingSf = false;
closesocket(listenfd);
return;
}
/*
如果队列中没有等待的连接,套接字也没有被标记为Non-blocking,accept()会阻塞调用函数直到连接出现;
如果套接字被标记为Non-blocking,队列中也没有等待的连接,accept()返回错误EAGAIN或EWOULDBLOCK。
*/

char ip[20] = { '\0' };
inet_ntop(AF_INET, &client_addr.sin_addr, ip, 16);
if (strcmp(ip, "127.0.0.1") == 0)//用于跳出假连接
{
if (flag)//1是getfile
isRecvingGf = false;
else
isRecvingSf = false;
closesocket(listenfd);
return;
}

//----------------------打开文件------------------------------------------------------
FILE* fp = NULL;
if (fopen_s(&fp, filename.c_str(), "wb") != 0)//要以二进制形式读写,这样兼容文件格式
{
std::cerr << "cannot open file " << filename << std::endl;
if (flag)//1是getfile
isRecvingGf = false;
else
isRecvingSf = false;
return;
}
std::chrono::system_clock::time_point time1 = std::chrono::system_clock::now();

int totalRecv = 0;
while (1)
{
int nRecv = recv(conn, recvbuf, buffSize, 0);
if (nRecv == SOCKET_ERROR)//copy出错
{
std::cerr << "connection to client has been failed" << std::endl;
if (flag)//1是getfile
isRecvingGf = false;
else
isRecvingSf = false;
return;
}
else if (nRecv == 0)//这种情况是对端close了,此时返回0。可能是意外close,也可能是发送完毕了
{
break;
}
totalRecv += nRecv;

size_t nWrite = fwrite(recvbuf, 1, nRecv, fp);//写文件

if (nWrite != nRecv || ferror(fp) != 0)
{
std::cerr << "failed to write file" << std::endl;
if (flag)//1是getfile
isRecvingGf = false;
else
isRecvingSf = false;
return;
}
}

fclose(fp);
std::chrono::system_clock::time_point time2 = std::chrono::system_clock::now();
int MB = totalRecv / (1024 * 1024);
int ms = int(std::chrono::duration_cast<std::chrono::milliseconds>(time2 - time1).count());
std::string result = "recv [" + filename + "] [" + std::to_string(totalRecv) + "] bytes (" + std::to_string(MB) + " MB) cost [" + std::to_string(ms) + "] ms\n";

if (fopen_s(&fp, filelog, "a") != 0)//写result
{
std::cerr << "cannot open file " << filename << std::endl;
if (flag)//1是getfile
isRecvingGf = false;
else
isRecvingSf = false;
return;
}
fwrite(result.c_str(), 1, result.size(), fp);
fclose(fp);

closesocket(conn);
closesocket(listenfd);

if (flag)//1是getfile
isRecvingGf = false;
else
isRecvingSf = false;
}

补充

现在消息都能发给服务器了,但是服务器不知道是聊天信息还是命令信息…所以,我们给聊天信息加一个关键字符,一方面我们改的代码少,一方面由于命令没有这个关键字符,所以只要有就能判定为聊天信息,不会冲突。这个关键字符是~,加在内容前面。

本来是这样的:

1
2
3
4
5
6
7
if (state == clientState::isChatting && cmdbuf[0] != '@')//如果在chatting,并且输入第一个不是@,判定为聊天内容直接发送
{
//send...
send(connfd, cmdbuf, strlen(cmdbuf), 0);//发送
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
}

现在是这样的:

1
2
3
4
5
6
7
8
if (state == clientState::isChatting && cmdbuf[0] != '@')//如果在chatting,并且输入第一个不是@,判定为聊天内容直接发送
{
//send...
string charstr = "~" + string(cmdbuf);
send(connfd, charstr.c_str(), int(charstr.size()), 0);//发送
memset(cmdbuf, 0, sizeof(cmdbuf));
cout << prompt << flush;
}

加两个小功能

  • 自动登录,在配置文件里写是user_id和user_password
  • 加一个打印自己的资源列表的命令(意识到这是蛮必要的)。myresource->19。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Client::autoLogin(SOCKET connfd)
{
if (config_map.find("user_id") == config_map.end() || config_map.find("user_password") == config_map.end())
return;//如果有一个没填就不管

cout << "Login automatically......" << endl;
string cmdstr = "login " + config_map["user_id"] + " " + config_map["user_password"] + "\n";
char recvbuf[128];
send(connfd, cmdstr.c_str(), int(cmdstr.size()), 0);//发送
recv(connfd, recvbuf, sizeof(recvbuf), 0);//同步接收,是阻塞的

if (strcmp(recvbuf, "success") == 0)
{
cout << "Login successfully!" << endl;
state = clientState::cmdLine;
prompt = promptMap[state];
}
else
{
cout << recvbuf << endl;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//run里面添加命令
case 19:
if (cmdvec.size() != 1)
{
cerr << "error> The number of parameters for [myresource] is wrong!" << endl;
break;
}
else
{
//只要打印就好了
string myresolist;
for (int index = 1; index < myresource_map.size(); index += 1)
{
myresolist += "[" + to_string(index) + "]" + "\t" + myresource_map[index] + "\n";
}
cout << myresolist << flush;
break;
}

现在,客户端差不多可以收尾了。

服务器的设计在下一章博客,封装好的代码也会放出来。