计算机网络第一次实验:提高要求:多人聊天
要求
-
设计一个多人聊天协议,要求实现选择不同的用户进行分组聊天。
-
设计多人聊天程序。
-
在Windows系统下,利用C/C++中的流式Socket对设计的程序进行实现。程序界面可以采用命令行方式,但需要给出使用方法。
-
对实现的程序进行测试。
-
撰写实验报告,并将实验报告和源码提交至本网站
基本功能概述
- 多人聊天通信,最多支持100人;
- TCP协议,流式套接字;
- 支持包括但不限于中文、英文等发送;
- 合理的用户上线下线机制;
- 正常的用户退出方式。
代码运行环境
VS2019,使用的包包括:
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>
#include<time.h>
#include<string>
#include<iostream>
#pragma comment (lib,"ws2_32.lib")
聊天协议
在该聊天室程序的两端server和client都遵循如下聊天协议,符合协议的对等性。
语法
报文包括三个数据段,从高位到低位,第一个数据段表示发送/接受时间,第二个数据段表示发送者的昵称,第三段表示发送的信息。数据段之间用一些文字相连,更加直观。报文结构如下所示:
发送消息,消息具体细节如下:
发送时间:2021/10/18 22:36:55 Monday
发送人:413123123
消息:3123123
接收消息,消息具体细节如下:
发送时间:2021/10/18 22:36:55 Monday
发送人:13132123
消息:312312312
其中规定,报文的最大总长度为512,姓名的最大总长度为16
语义及时序
服务器端
运行server.cpp,服务器建立socket,执行bind操作后,进入listen状态。
在listen状态中,server等待client的connect请求,一旦受到connect请求,就会执行accept,输出连接成功的消息,传入欢迎socket和用户的连接socket,建立一个新的服务器连接socket。此时客户端会进行多次连接的操作,每次连接成功都会输出连接成功的消息。党所有用户连接完成后,开始进行收发消息的操作。这里为了同时支持多个client在线,需要用到多线程,即随机创建一个没有被使用过的线程号,使用CreateThread创建一个新的线程。在这个线程的入口地址中处理对应的client请求。
在创建完新线程后,在线程中接收client发送的报文信息,若收到了某个client发送的消息,则执行recvFun函数,在服务器端显示已经收到该信息并将该信息的具体细节显示出来。
同时为了让其他的客户端也显示出该客户端的消息,服务器端也会进行发送操作。此时服务器端作为一个中转端,识别出具体发送消息的客户端并将之前从该客户端收到的消息转发到其他的客户端,在其他客户端显示消息。
用户端
运行client.cpp,先输入这一端聊天者的昵称,然后保存昵称。接下来服务器建立socket,执行bind操作,并与客户端进行连接,若成功能连接到客户端,则输出连接成功的消息。此时正式开始两人聊天过程。
在与客户端完成连接后,在线程中接收client发送的报文信息,同时在用户端显示已经收到该信息并将该信息的具体细节显示出来。
因为recv是阻塞的,所以一旦接受到服务器发送的消息,就会利用recvFun函数接收服务器端发送的具体消息信息以及实际报文。然后按照规则将该消息输出到屏幕上。
与此同时,用户端也可以进行发送操作。当一方输入需要发送的消息时,sendFun函数会将发送消息时的时间,发送人,和具体消息的信息进行打包,并将该消息整体发送到客户端,由客户端接收。因为此时send和recv在两个不同的线程之中,所以不会造成干扰。
核心代码片段设计
设置变量,设定版本和初始化
void hThread();
int flag = 0;
char name[16] = "默认姓名1";//初始化姓名
HANDLE h0[100],h1, h2;//线程句柄
int u = 1;
int main()
{
SOCKET serverSocket;//监视的套接字
SOCKADDR_IN newAddr;//保存客户端的socket地址信息
SOCKADDR_IN addr;//地址结构体,包括ip port(端口)
WSADATA data;//存储被WSAStartup函数调用后返回的Windows Sockets数据
WORD version;//socket版本
int info;
//在使用socket之前要进行版本的设定和初始化
version = MAKEWORD(2, 2);//设定版本
info = WSAStartup(version, &data);
//应用程序或DLL只能在一次成功的WSAStartup()调用之后才能调用进一步的Windows Sockets API函数。
//有套接字的接口才能进行通信
创建Socket
//1.创建socket
serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//AF_INET使用IPV4地址,SOCK_STREAM使用流传输,IPPROTO_TCP使用TCP协议
addr.sin_addr.S_un.S_addr = htonl(ADDR_ANY);//表示任何的ip过来连接都接受
addr.sin_family = AF_INET;//使用ipv4的地址
addr.sin_port = htons(11111);//设定应用占用的端口
服务器段Cli.cpp创建Socket
sockClient = socket(AF_INET, SOCK_STREAM, 0);
//要连接的服务器的ip,因为现在服务器端就是本机,所以写本机ip
//127.0.0.1一个特殊的IP地址,表示是本机的IP地址
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
//端口要与服务器相同
addr.sin_port = htons(11111);
//用IPV4地址
addr.sin_family = AF_INET;
服务器端绑定端口号并开始监听,等待客户端连接
listen(serverSocket, 100);
cout << "开始聊天,等待对方连接.........." << endl;
int len = sizeof(SOCKADDR);
//accept是一个阻塞函数,如果没有客户端请求,连接会一直等待在这里
//该函数会返回一个新的套接字,这个新的套接字是用来与客户端通信的套接字,之前那个套接字是监听的套接字
服务器端传输信息过程
while (1) {
//4.接受来自客户端的连接请求
sockConn[u] = accept(serverSocket, (SOCKADDR*)&newAddr, &len);//接受客户端的请求
cout << "连接成功......" << endl;
u += 1;
for (int i = 1; i <=u; i++) {
h0[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)hThread, NULL, 0, NULL);
//WaitForSingleObject(h0[i], INFINITE);
}
服务器端线程处理函数,负责中转客户端的信息
void hThread() {
for (int i = 1; i <= u; i++) {
char buf[512];
if (recv(sockConn[i], buf, 512, 0) > 0) {
if (strcmp(buf, "End") == 0) {
cout << "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b";
cout << "已结束聊天" << endl;
for (int j = 1; j <= u; j++) {
if (j == i)
continue;
else
send(sockConn[j], buf, strlen(buf) + 1, 0);
}
// break;
}
else {
cout << "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b";
cout << "接收消息,消息具体细节如下:" << endl;
cout << buf << endl;
cout << endl;
cout << name << "请输入消息:";
for (int j = 1; j <= u; j++) {
if (j == i)
continue;
else
send(sockConn[j], buf, strlen(buf) + 1, 0);
}
}
}
else
continue;
}
}
客户端进行姓名输入以及收发消息
cout << "请输入你的用户名:";
cin >> name;
//主动连接服务器
cout << "尝试与对方进行连接" << endl;
while (1) {
//2.连接指定计算机端口
if (connect(sockClient, (SOCKADDR*)&addr, sizeof(SOCKADDR)) == 0) {
cout << "已连接!" << endl;
//创建线程后立即执行
//向socket中发送/接受信息
h1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)sendFun, NULL, 0, NULL);
h2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)recvFun, NULL, 0, NULL);
WaitForSingleObject(h1, INFINITE);//会阻塞,直到线程运行结束
WaitForSingleObject(h2, INFINITE);//会阻塞,直到线程运行结束
}
else
;
}
客户端发送线程处理信息
void sendFun()
{
char buf[512];
while (1)
{
char buf1[512];
time_t t = time(0);
char tmp[64];
cout << endl;
cout << name << "请输入消息:";
cin >> buf1;
if (strcmp(buf1, "bye!") == 0) {
cout << "聊天已结束!" << endl;
send(sockClient, "End", 4, 0);
break;
}//判断聊天结束
cout << "发送消息,消息具体细节如下:" << endl;
strftime(tmp, sizeof(tmp), "%Y/%m/%d %X %A ", localtime(&t));
strcpy(buf, "发送时间:");
strcat(buf, tmp);
strcat(buf, "\n发送人:");
strcat(buf, name);
strcat(buf, "\n消息:");
//strcat(buf, "\n");
strcat(buf, buf1);
cout << buf << endl;
//发送数据
send(sockClient, buf, strlen(buf) + 1, 0);
}
}
客户端接收线程处理信息
void recvFun()
{
char buf[512];
//接收服务发送的数据
while (1)
{
int n;
if (recv(sockClient, buf, 512, 0) > 0) {
if (strcmp(buf, "End") == 0) {
cout << "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b";
cout << "对方已结束聊天" << endl;
break;
}
else {
cout << "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b";
cout << "接收消息,消息具体细节如下:" << endl;
cout << buf << endl;
cout << endl;
cout << name << "请输入消息:";
}
}
else {
break;
}
}
}
程序实验过程及实验结果
程序开始时,同时打开服务端与多个客户端。客户端输入聊天时的姓名,输入完毕后等待对方连接完毕。
客户端可以按照输入姓名的顺序依次进行聊天,发送信息,此时所有客户端均会受到该客户端发送的消息。
发送消息时发送方会收到如下格式消息:
张三请输入消息:你好!
发送消息,消息具体细节如下:
发送时间:2021/10/21 16:45:22 Tuesday
发送人:张三
消息:你好!
接收方会受到如下格式消息:
接收消息,消息具体细节如下:
发送时间:2021/10/21 16:59:35 Tuesday
发送人:张三
消息:你好!
具体对话过程如下:
要终止群聊对话,一方可以发送"bye!"消息,此时向所有服务器端和客户端发送End消息,表示一方已经终止聊天,此时对方收到该消息,显示聊天结束。效果如下图所示: