要求和基本步骤

NKU COMPUTER NETWORK LAB1

实验要求

使用流式Socket,设计一个两人聊天协议,要求聊天信息带有时间标签。请完整地说明交互消息的类型、语法、语义、时序等具体的消息处理方式。

具体实现

关于协议

这一点个人随意设计,大致思路是,除了聊天所发出的消息,加上一些其他的字段,比如源IP和目标IP,或者随便加点什么然后称这个字段是检验位。也可以搞一些编码转换或者和IP有关的的东西。

具体实现

单纯的二人聊天比较简单,就是按照老师的PPT或者网上可参考的程序,在server端调用socket, bind, listen, accept, 在client端调用socket, connect, 然后再在while循环中调用send, recv 即可。当然,这里需要注意的是,此时的聊天程序只能是one by one的聊天,并不能实现一个人发送多条消息,而且也不能接收多条消息,甚至只能由某一端先说话,是一种比较笨的实现方式。这主要是由于阻塞问题。如果非要这样实现,可行的方式是在时间上进行调整,使缓冲区重新变为空。

解决方法是采用多线程的方式,将发送和接收消息写成两个线程,这样双方互不干扰,就不会有阻塞的问题。

当然,如果要实现多人聊天,那么情况就会变得复杂,server端此时要实现转发的功能,而且在某个客户端上线或者下线,以及随之带来的标号变化都需要考虑一下。同时,如果像18级丁神考虑了私聊和群发的话,也是有很多新东西要考虑。这时如果设计的不好又会再次出现阻塞这个问题,这样会导致只有每一个客户端都发完消息,消息才会被显示,总之各种小坑。

所需函数均已在PPT中给出,但是这门课的实验,一没有实验指导书,二没有代码框架,如果只看PPT其实是很难顺利完成实验的,因为这门课对实验的设计显然是非常不完备的。

附上一些老师给出的代码,完整代码将在课程结束后一并放出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

void main()
{
WSAStartup(wVersionRequested, &wsaData);

SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

recv(sockClient, recvBuf, 50, 0);
send(sockClient, "hello", sendlen, 0);

closesocket(sockClient);
WSACleanup();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void main()
{
WSAStartup(wVersionRequested, &wsaData);
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
listen(sockSrv, 5);
while (1) {
SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
hThread = CreateThread(NULL, NULL, handlerRequest,LPVOID(sockConn), 0, &dwThreadId);
CloseHandle(hThread);
}
closesocket(sockSrv);
WSACleanup();
}

DWORD WINAPI handlerRequest(LPVOID lparam)
{
SOCKET ClientSocket = (SOCKET)(LPVOID)lparam;
send(ClientSocket, sendBuf, strlen, 0);
recv(ClientSocket, recvBuf, 50, 0);
closesocket(ClientSocket);
return 0;
}

ps: 老师给的这个多线程server端并不好,建议重构。

附上一些参考链接,有助于从零开始理解和写出程序:一个讲解原理的一个csdn的博客
B站上也有一些视频也不错,都是很值得推荐的,跟着做绝对能做出来。

如果你想要自己完成,那么看到这里已经够了。 如果你想要采取某些技巧完成这个作业,然后拿出更多时间干更有意义的事情,请往下看。

我的程序

分了三个版本,3.0最难但仍然没有拿到满分,不过考虑到本身实验也占比不多,就无所谓了,拿出时间去学学uCORE是更有意义的事情。

1.0

初代版本比较简单,所实现的功能非常简陋。

具体介绍如下:

  • 采用server和client对话,并不需要转发

  • 只能由一端先开始发送,另一端必须在接收消息之后才能发送

  • 一次只能发送一条,不能连续发送消息

  • 也不能连续接收消息

  • 但是很简单,交了能拿90分,也挺好的

代码很简单,只需要小幅度老师给的程序修改即可。里面写了一些注释,用于说明。

首先是一段改变控制台的颜色的代码,我也不懂具体什么原理,反正确实能用,如果像弄懂,请把代码直接复制到搜索引擎进行搜索,应该第一个csdn的博客就是。并不需要在程序中实现这种花里胡哨的功能,我当时仅仅是为了分开发送和接收才加的,都是白色的看着太累了。

Client
1
2
3
4
5
6
7
void SetColor(int fore=7,int back=0)
{
unsigned char m_color = fore;
m_color += (back << 4);
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), m_color);
return;
}

下面进入正文了朋友们。

Client
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
void main()
int main() {
int FIRST_JUDGE;
WSADATA wsaData;
FIRST_JUDGE = WSAStartup(MAKEWORD(2, 2), &wsaData);
//指定版本wVersionRequested=MAKEWORD(a,b)。MAKEWORD()是一个宏把两个数字组合成一个WORD,无符号的短整型
if (FIRST_JUDGE != 0) {
//等于没有链接成功,失败了。
SetColor(0,12);
cout << "初始化socket都失败了宝贝儿,你穆哥建议还是先回家再好好学学吧!(¬_¬)" << endl;
return 0;
}

//创建socket。这里我们使用流式socket。
socket_client1 = socket(AF_INET, SOCK_STREAM, 0);

//初始化客户端地址
addr_client1.sin_addr.s_addr = inet_addr("127.0.0.1");//把我们本机的地址转换成网络字节二进制值序
addr_client1.sin_family = AF_INET;//使用ipv4
addr_client1.sin_port = htons(8000);//转换函数,也是转换成网络字节序。
//初始化地址
addr_serve1.sin_addr.s_addr = inet_addr("10.130.122.241");//把我们本机的地址转换成网络字节二进制值序
addr_serve1.sin_family = AF_INET;//使用ipv4
addr_serve1.sin_port = htons(8000);//转换函数,也是转换成网络字节序。
if (connect(socket_client1,(SOCKADDR *) &addr_serve1,sizeof(addr_serve1) )!=SOCKET_ERROR)
{
strcpy(send_buf1, "你好啊!我是客户端!我已经连接到你了!");
send(socket_client1, send_buf1, 2048, 0);
}
else {
SetColor(0,12);
cout << "连接失败了宝贝儿,快查查是不是没启动服务端呢?|_(._.)_|" << endl;
return 0;
}

while (1) {
SetColor(0,12);
recv(socket_client1,receive_buf1,sizeof(receive_buf1),0);
//判断是否对方要退出
if (strlen(receive_buf1) == 0)
{
cout << "服务器选择结束聊天了!再见哟!(//▽//)" << endl;
break;
}
else {
time_t now_time = time(NULL);
tm *t_tm = localtime(&now_time);
cout << "服务端发的:" << receive_buf1 << " " << "收到时间: " << asctime(t_tm);
}
cin.getline(input_buf1, 2048, '\n');
if (!strcmp("quit", input_buf1))
{
send(socket_client1, {}, 2048, 0);
cout << "您已选择结束聊天!(//▽//)" << endl;
break;
}
else{
strcpy(send_buf1, input_buf1);
send(socket_client1, send_buf1, 2048, 0);
}
Sleep(30);
}
closesocket(socket_client1);
WSACleanup();
}
Server
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
int main() {
int FIRST_JUDGE;
WSADATA wsaData;
FIRST_JUDGE = WSAStartup(MAKEWORD(2, 2), &wsaData);
//指定版本wVersionRequested=MAKEWORD(a,b)。MAKEWORD()是一个宏把两个数字组合成一个WORD,无符号的短整型
if (FIRST_JUDGE != 0) {
//如果失败了
SetColor(0,12);
cout << "初始化socket都失败了宝贝儿,建议还是先回家再好好学学吧!(¬_¬)" << endl;
return 0;
}

//创建socket。这里我们使用流式socket。
socket_serve = socket(AF_INET, SOCK_STREAM, 0);
//初始化客户端地址
addr_serve.sin_addr.s_addr = inet_addr("10.130.122.241");//把我们本机的地址转换成网络字节二进制值序
addr_serve.sin_family = AF_INET;//使用ipv4
addr_serve.sin_port = htons(8000);//转换函数,也是转换成网络字节序。


if (bind(socket_serve, (SOCKADDR *) &addr_serve, sizeof(SOCKADDR)) == -1) {
SetColor(0,12);
cout << "绑定这一步都失败了宝贝儿,建议还是先回家再好好学学吧!(¬_¬)" << endl;
return 0;
}
//绑定
bind(socket_serve, (SOCKADDR *) &addr_serve, sizeof(SOCKADDR));

//监听
listen(socket_serve, 5);
socket_client = accept(socket_serve, (SOCKADDR *) &addr_client, &size_addr);

//接受,返回的是一个socket
if (socket_client != INVALID_SOCKET)//判断连接成功
{
SetColor(0,12);
cout << "LOGGING: 成功连接上啦!(^_^)" << endl;
// strcpy(send_buf, "你好啊!我是客户端!你已经连接到我了!");
// send(socket_client, send_buf, 2048, 0);
}
else {
SetColor(0,12);
cout << "连接失败了宝贝儿,快查查是不是没启动客户端呢?|_(._.)_|" << endl;
return 0;
}

while (1) {
char receive_buf_loop[buf_size]={};
// int NetTimeout = 500; //超时时长
// setsockopt(socket_client, SOL_SOCKET,SO_RCVTIMEO,(char *)&NetTimeout,sizeof(int));
recv(socket_client, receive_buf_loop,2048,0);
//判断是否对方要退出
if (strlen(receive_buf_loop) == 0)
{
cout << "客户端选择结束聊天了!再见哟!(//▽//)" << endl;
break;
}
else {
time_t now_time = time(NULL);
tm *t_tm = localtime(&now_time);
cout << "客户端发的:" << receive_buf_loop << " " << "自己收到时间: " << asctime(t_tm);
}
cin.getline(input_buf, 2048, '\n');
if (!strcmp("quit", input_buf))
{
// strcpy(send_buf, '\0');
send(socket_client, {}, 2048, 0);
cout << "您已选择结束聊天!(//▽//)" << endl;
break;
}
else{
strcpy(send_buf, input_buf);
send(socket_client, send_buf, 2048, 0);
}
Sleep(30);
}
closesocket(socket_serve);
WSACleanup();
}

以上是基本功能,如果大三上编译和操作系统还有体系结构或者恶意代码的压力很大的话,直接改改我这个就好了。计网这些实验都不算精品,没有经过精心的设计,因此不值得认真研究。看看uCORE吧朋友们,那玩意很深。

2.0

2.0版本依旧是二人对话,而且依旧是服务端和客户端对话,唯一的区别就是弄了多线程,而且自己假模假式的设计了一个协议——随便加了点字符上去。这样每一个人都可以开始发送和接收,而且发送接收多条都可以,很随心所欲。

首先是一个解析报文的函数。就是在所发送的消息之前随便加了个字段,然后就发出去了,这样在接收那边就要用这个解析一下。

Parse
1
2
3
4
5
6
7
8
9
string parseMessage (char *s) {
string str(s);
int pos = str.find ( "mycNB!" , 0 , 6) ;
if ( pos == -1) {
exit(0);
}
string chat = str.substr (pos + 6,str.length ()) ;
return chat ;
}

然后把两个线程的内容放上来,哪一端都是这俩线程函数。主函数和1.0都大差不差的,不放上来了。

Send
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
DWORD WINAPI Send(LPVOID thesocket) {
SOCKET * sock = (SOCKET*)thesocket;
char sendBuf[BUF_SIZE] = {};
char inputBuf[BUF_SIZE] = {};
while (1) {
//printf("Input a string: ");
cin.getline(inputBuf, 2048, '\n');
strcpy(sendBuf, "mycNB!");
strcat(sendBuf, inputBuf);
int t = send(*sock, sendBuf, strlen(sendBuf), 0);
if (strcmp(inputBuf, "imquit") == 0)
{
SYSTEMTIME st = { 0 };
GetLocalTime(&st);
closesocket(*sock);
SetColor(0,12);
cout << "您已于" << st.wHour << "时" << st.wMinute << "分" << st.wSecond << "秒选择结束聊天了!再见哟!(//▽//)" << endl;
//printf("您已于%s时%s分%s秒选择结束聊天了!再见哟!(//▽//)",st.wHour,st.wMinute,st.wSecond);
exit(0);
}
if (t > 0) {
SetColor(1,0);
SYSTEMTIME st = { 0 };
GetLocalTime(&st);
cout << "消息已于" << st.wHour << "时" << st.wMinute << "分" << st.wSecond << "秒成功发送\n" ;
cout << "-------------------------------------------------------------" << endl;
}
memset(sendBuf, 0, BUF_SIZE);
}
}
Recv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DWORD WINAPI Recv(LPVOID thesocket) {
char recvBuf[BUF_SIZE] = { 0 };
SOCKET *sock = (SOCKET*)thesocket;
while (1) {
SetColor();
int t = recv(*sock, recvBuf, BUF_SIZE, 0);
if (strcmp(parseMessage (recvBuf).data(), "imquit") == 0)
{
SetColor(0,12);
SYSTEMTIME st = { 0 };
GetLocalTime(&st);
closesocket(*sock);
cout << "您的小宝贝(服务端)已于" << st.wMonth <<"月"<< st.wDay << "日" << st.wHour << "时" << st.wMinute << "分" << st.wSecond << "秒选择结束聊天了!再见哟!(//▽//)" << endl;
exit(0);
return 0L;
}
if (t > 0) {
time_t now_time = time(NULL);
tm *t_tm = localtime(&now_time);
SetColor(14,0);
cout << asctime(t_tm) << "收到您的小可爱(客户端)发来的消息:";
cout<<parseMessage (recvBuf)<<endl;
cout << "-------------------------------------------------------------" << endl;
}
memset(recvBuf, 0, BUF_SIZE);
}
}

这个版本借鉴 (抄袭) 了朱学长的代码,可以去git上参考他的,写的比较直观,没有我这些魔改内容。

3.0

好吧,这个就是自己重写的了,因为这种有加分项的开放东西贼恶心,因此被动卷了一波,不过还是写了个群发就收手了。

这里有几点,必须先指定需要多少人聊天,然后等每一个人都加入聊天室才能开始聊天。同时,退出功能也不太完美,一个退出会导致所有人一起退出(这是我就这么设计的,因为本身这个多人聊天是从二人聊天扩展过来的,二人的聊天一旦一个人退出了你说另一个人不退出他要干嘛?更主要的原因是更合理的退出方式还需要改一些新东西,没啥时间了quq)。

首先是个结构体

本质上这玩意和2.0的解析函数一样,只不过对OOP那一套不熟悉,所以用一些结构体的东西来表示了。ps: 为什么不能用python语言,python的函数就可以返回多个值,就不用写这破玩意了。python你不让调库不是一样很难,非得用Cpp锻炼?黑砖的题就是python的啊,很不理解。

Parse
1
2
3
4
5
6
7
8
9
10
11
12
13
struct message {
string id;
string chat;
};

struct message parseMessage(char *s) {
message msg;
string str(s);
int pos = str.find("mycNB!", 0, 6);
msg.id = str.substr(pos + 6, 1);
msg.chat = str.substr(pos + 7, str.length());
return msg;
}

相信我不需要做过多解释,就是拆字符串。。。

然后是线程

客户端与2.0相同,在此不做说明,服务端改成一个线程,专门负责转发。其中一些判定相信看看就明白了,在此也不做过多说明了。

Transmit
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
DWORD WINAPI Recv(LPVOID thesocket) {
char recvBuf[BUF_SIZE] = {};
SOCKET sock = sockConn[(long long) thesocket];
if (sock != INVALID_SOCKET) {
while (1) {
recv(sock, recvBuf, BUF_SIZE, 0);
if (recvBuf[0]) {
message msg = parseMessage(recvBuf);
SetColor(14, 0);
if (strcmp(msg.chat.data(), "imquit") == 0) {
cout <<"有一位用户选择下线(*_*),会议结束";
for (int i = 0; i < chatnumber; i++) {
if (sock != sockConn[i]) {
send(sockConn[i], recvBuf, 2048, 0);}
}
closesocket(sockConn[(long long) thesocket]);
closesocket(sock);
sockConn[(long long) thesocket]=INVALID_SOCKET;
system("pause");
exit(0);
} else {
time_t now_time = time(NULL);
tm *t_tm = localtime(&now_time);
cout << asctime(t_tm) << "收到您的小可爱"<<msg.id<<"号发来的消息:";
cout<<msg.chat<<endl;
cout<<"-----------------------------------------------"<<endl;
for (int i = 0; i < chatnumber; i++) {
if (sock != sockConn[i]) {
send(sockConn[i], recvBuf, 2048, 0);
}
}
}
memset(recvBuf,0,BUF_SIZE);
}
else{return 0;}
}
}
return 0;
}

主函数

server的主函数在此展示出来,主要是一些初始化和监听的内容。

ServerMain
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
int main() {
SetColor(0, 12);
cout << "需要" <<chatnumber<<"位用户才能开始聊天哦亲!(^-^)"<< endl;
SetColor();
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) ==
0) //指定版本wVersionRequested=MAKEWORD(a,b)。MAKEWORD()是一个宏把两个数字组合成一个WORD,无符号的短整型
{
SetColor(0, 12);
cout << "初始化socket成功了亲" << endl;
} else {
SetColor(0, 12);
cout << "初始化socket都失败了宝贝儿,你穆哥建议还是先回家再好好学学吧!(¬_¬)" << endl;
return 0;
}
//创建套接字
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in addrSrv;
memset(&addrSrv, 0, sizeof(addrSrv)); //每个字节都用0填充
addrSrv.sin_family = AF_INET; //使用IPv4地址
addrSrv.sin_addr.s_addr = inet_addr("127.0.0.1"); //把我们本机的地址转换成网络字节二进制值序
addrSrv.sin_port = htons(8000); //端口,好像是在一个区间之内就可以.//转换函数,也是转换成网络字节序
int z = bind(sockSrv, (SOCKADDR *) &addrSrv, sizeof(addrSrv));
if (z == -1) {
SetColor(0, 12);
cout << "绑定这一步都失败了宝贝儿,建议还是先回家再好好学学吧!(¬_¬)" << endl;
return 0;
} else {
SetColor(0, 12);
cout << "绑定成功了亲(^_^)" << endl;
}
//进入监听状态
//接收客户端请求
SOCKADDR addrCli[2];
int nSize = sizeof(SOCKADDR);
if (listen(sockSrv, 5) == 0) {
SetColor(0, 12);
cout << "正在监听之中哦亲(^_^)" << endl;
for (int i = 0; i < chatnumber; i++) {
//为每个客户端分配一个socket连接,将客户端的相关信息存储在addrCli中
sockConn[i] = accept(sockSrv, (SOCKADDR *) &addrCli[i], &nSize);
if (sockConn[i] != INVALID_SOCKET)
{
cout << "用户" << i << "进入聊天" << endl;
char buf[12] = "你的id是:";
buf[10] = 48 + i;
buf[11] = 0;
send(sockConn[i], buf, 50, 0);
}
}
} else {
SetColor(0, 12);
cout << "监听都失败了宝贝儿,你穆哥建议还是先回家再好好学学吧!(¬_¬)" << endl;
return 0;
}
//开启多线程
HANDLE hThread;
while (1) {
for (int i = 0; i < chatnumber; i++) {
hThread = CreateThread(NULL, 0, Recv, LPVOID(i), 0, NULL);
WaitForSingleObject(hThread, 2000);
CloseHandle(hThread);
}
}
closesocket(sockSrv);
WSACleanup();
return 0;
}

小小总结

以上就是我所写的三个版本的全部内容。需要注意的是,博客之中并没有给出变量的声明和对变量的具体说明这种内容,同时由于我非常糟糕的变量命名习惯,需要去看源码,以免造成什么误解。

整体来说,计算机网络这门课的第一个实验并不难,也不深,甚至学不到什么东西,就是调用了一堆你也不太明白底层在干什么的函数,然后写了一些乱七八糟的内容,反正最后能聊天了。基本上找一个可以参考的内容,按部就班地做下来,也就没问题了。网络上可供参考的内容太多。整体上来说小的坑、绊人的地方还是有一些,尤其是对于多线程那块,如果之前没学过可能会报一些意想不到的错误。由于本作业经历多次迭代,已经陷入到一种不可修改的地步,修改任何一行都有可能造成程序的崩溃,因此我在实现了群发之后,也就收手了。

写在最后

如果3.0仍不满足你对分数的要求,可以自行探索相关内容或者顺着我的github上follow的那些人,他们之中有很多大神,都开源了自己的代码。希望本篇博客对你有帮助。我开源了自己的代码,如果对你有帮助的话,请给我STAR!谢谢!

对于本课程,我想说实验的质量确实有待提高,虽然我校的大多数课的实验都不是很完备,但是连个实验指导书都没有就非常过分了。因此,我的建议还是自学,因为也不查重,只要看懂代码,稍作修改然后在给助教讲的时候讲清楚并正确回答问题即可。只要别很头铁地直接复制粘贴然后也不看代码,都能很轻松的通过。那么,把时间腾出来,去做更有意义的事情,才是大三上这个关键时期要做的。

github仓库