在我们的日常生活中,网络聊天是必不可缺少的一个重要组成部分。而时常使用平台开发软件的我们,是否有去想过其中的原理是什么,我们又该如何去实现呢?
实验内容
编程完成TCP连接下的多人聊天室(考虑局域网、互联网两种实验环境),回答实验提出的问题及实验思考。即完成一个以TCP连接为基础,支持以下五种功能的多人聊天室:
1.登录功能。每个用户拥有自己的用户名,服务器存储和管理相关用户信息。
2.通知功能。用户会收到其他用户的上线下线通知。
3.聊天功能。用户可以实现一对一私聊或者对所有人的公屏广播。
4.呼叫功能。用户可以查询指定用户的在线状态,并告知被查询人某个用户正在找他。
5.群聊功能。每个用户可以创建最多一个群聊,可以加入退出创建解散群聊,在群聊中的发言会发送到所有在群聊的用户。
实验思路
Socket框架介绍
首先,要在电脑上实现TCP连接通信,socket 肯定少不了, Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
如果你是在 DEV C++ 上进行 socket 编程,那别忘了在工具-编译器选项中在编译时加入以下命令里加入 -lwsock32
:
接下来给出socket编程的大体框架。
服务器端:
/* TCPdtd.cpp - main, TCPdaytimed */
#include <stdlib.h>
#include <stdio.h>
#include <winsock2.h>
#include <time.h>
#include "conio.h"
#include <windows.h>
#include <process.h>
#include <math.h>
#include <set>
#define QLEN 5
#define WSVERS MAKEWORD(2, 0)
#define BUFLEN 2000 // 缓冲区大小
#pragma comment(lib,"ws2_32.lib") //winsock 2.2 library
using namespace std;
SOCKET msock, ssock; //master & slave sockets
struct sockaddr_in fsin;
struct sockaddr_in Sin;
/*------------------------------------------------------------------------
* main - Iterative TCP server for DAYTIME service
*------------------------------------------------------------------------
*/
int main(int argc, char *argv[]) {
/* argc: 命令行参数个数, 例如:C:\> TCPdaytimed 8080
argc=2 argv[0]="TCPdaytimed",argv[1]="8080" */
int alen; /* from-address length */
WSADATA wsadata;
char service[] = "5050";
WSAStartup(WSVERS, &wsadata); //加载 winsock 2.2 library
msock = socket(PF_INET, SOCK_STREAM, 0); //生成套接字。TCP协议号=6, UDP协议号=17
memset(&Sin, 0, sizeof(Sin));
Sin.sin_family = AF_INET;
Sin.sin_addr.s_addr = INADDR_ANY; //指定绑定接口的IP地址。INADDR_ANY表示绑定(监听)所有的接口。
Sin.sin_port = htons((u_short)atoi(service)); //atoi--把ascii转化为int,htons - 主机序(host)转化为网络序(network), s(short)
bind(msock, (struct sockaddr *)&Sin, sizeof(Sin)); // 绑定端口号(和IP地址)
listen(msock, 5); //队列长度为5
//Do Something......
(void) closesocket(msock);
WSACleanup(); //卸载载 winsock 2.2 library
return 0;
}
用户端:
/* TCPClient.cpp -- 用于传递struct */
#include <stdlib.h>
#include <stdio.h>
#include <winsock2.h>
#include <string.h>
#include <time.h>
#include <windows.h>
#include <process.h>
#include <math.h>
#define BUFLEN 2000 // 缓冲区大小
#define WSVERS MAKEWORD(2, 0) // 指明版本2.0
#pragma comment(lib,"ws2_32.lib") // 指明winsock 2.0 Llibrary
/*------------------------------------------------------------------------
* main - TCP client for DAYTIME service
*------------------------------------------------------------------------
*/
SOCKET sock; // socket descriptor
// int cc; // recv character count
char *packet = NULL; // buffer for one line of text
char *pts,*input;
HANDLE hThread;
int main(int argc, char *argv[])
{
time_t now;
(void) time(&now);
pts = ctime(&now);
char host[] = "127.0.0.1"; /* server IP to connect */
char service[] = "5050"; /* server port to connect */
struct sockaddr_in sin; /* an Internet endpoint address */
WSADATA wsadata;
WSAStartup(WSVERS, &wsadata); /* 启动某版本Socket的DLL */
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons((u_short)atoi(service)); //atoi:把ascii转化为int. htons:主机序(host)转化为网络序(network), s--short
sin.sin_addr.s_addr = inet_addr(host); //如果host为域名,需要先用函数gethostbyname把域名转化为IP地址
sock = socket(PF_INET, SOCK_STREAM, 0);
connect(sock, (struct sockaddr*)&sin, sizeof(sin));
//Do Someting......
CloseHandle(hThread);
closesocket(sock);
WSACleanup(); /* 卸载某版本的DLL */
printf("按回车键继续...");
getchar();
return 0; /* exit */
}
其中 Do Something
的部分就是来补充我们聊天室具体功能实现的地方。
结构布局
如上图所示,考虑到客户的数量比较少,我们直接采用“一个中心”的设计原则,即一个中心服务器处理所有的消息,所有的客户端都必须与服务器端建立TCP连接,客户端之间不建立实际的TCP连接,客户端所有的行为和消息都会经过服务器端。
这种设计模式有很多好处:一是服务器端可以获得全局的信息,便于服务器端根据整体的状态进行管理;二是将客户端之间隔离开,保护了客户之间的隐私,也保证了客户之间的安全;三是修复漏洞时比较便利,容易定位到问题所在位置,进行精准修复。
多用户&多线程
我们知道,一个客户端和服务器建立连接后,随时有可能发信息换言之连接应该是一直存在的。而我们要实现多用户要和同一个聊天室建立连接发送信息,换言之我们的服务器程序要同时和很多个用户保持长时间的连接,那我们一般写的服务器程序肯定是顶不住的。这就相当于在服务器程序中,由于要和A有个长时间连接,我们可以想象成一个 while(1)
的循环;这时B也想建立连接只能等A结束连接后,这和我们期望中的聊天室肯定是不一样的。为了解决这个问题,我们考虑使用多线程。
多线程是指从软件或者硬件上实现多个线程并发执行的技术。简单来说就是能够在同一时间执行多于一个线程,每个线程处理各自独立的任务,进而简化处理异步事件的代码,改善响应时间,提升整体处理性。这样我们就能为每个用户与服务器的通信创建一个线程,避免上面说到的问题。
一个进程的所有信息对该进程的所有线程都是共享的,包括可执行代码、程序的全局内存和堆内存、栈以及文件描述符。因此,相比进程与进程之间,线程之间的通信免去了繁杂的规则,但同时也面临着线程同步的问题,需要多加注意。
信息结构
由于我们要实现五种功能,为了方便起见,我们设计了一套通信语言能够让服务器快速知道用户想让服务器帮忙干什么,定义如下:
@name:message
代表我想给名叫 $name$ 的用户发送一条信息 $message$,如果name == ALL
,就代表公屏广播这条消息。@@group:message
代表我想给名叫 $group$ 的群组中的所有用户发送一条信息 $message$。?name
代表我想查询一下名叫 $name$ 的用户是否在线,若在线向其发送通讯请求。*group
代表我想创建一个名叫 $group$ 的群组,若原来我已经创建了一个群组且未解散,则先解散原群组再创建一个名叫 $group$ 的群组。+group
代表我想加入名叫 $group$ 的群组。-group
代表我想退出名叫 $group$ 的群组,若该群组是我创建的则解散它。exit
代表我想退出聊天室。
代码实现
我们的代码将分为服务器端和用户端分别进行。
服务器端
与客户建立连接
首先作为一个服务器,我们肯定要一直侦听,看看有没有人愿意和我建立连接。一旦发现用户发送建立连接的请求,我们要做三件事:
- 接受请求,并利用socket建立TCP连接。
- 记录用户数量
number++
。 - 开一个线程,在该线程上处理与新用户的通信。
所以我们的程序实现如下:
while(1) { //检测是否有按键
alen = sizeof(struct sockaddr);
ssock = accept(msock, (struct sockaddr *)&fsin, &alen);//接受请求
number++;//用户人数++
hThread[number] = (HANDLE)_beginthreadex(NULL, 0, Chat, NULL, 0, &threadID); //建立线程
}
用户上线及广播
在用户与我们建立连接后,用户会通过连接给我们发送其昵称,我们需要发送欢迎信息并向所有的用户广播其上线信息。
具体来说我们需要做三件事:
- 接受用户的昵称信息,保存至我们的
name[]
数组中。 - 向该用户发送欢迎短信。
- 向所有用户发送该用户上线信息。
所以我们程序实现如下:
recv(sock, buf3, BUFLEN, 0); //cc为接收的字符数
if(true) {//恒执行
strcpy(name[nownumber], buf3);
sprintf(buf2, "%s,欢迎加入聊天室", name[nownumber]);
(void) send(sock, buf3, sizeof(buf3), 0);
sprintf(buf2, "客户<%s> 加入了聊天室 \n", name[nownumber]);
printf("%s ", buf2);
printf("\t将自动把此数据发送给所有客户 \n");
for(int i = 0; i <= number; i++) {
if(sockets[i] != NULL && sockets[i] != sock) { //给所有在线的且不是该上线用户的用户广播
(void) send(sockets[i],buf2, sizeof(buf2), 0);
printf(" 发送至用户<%s>成功\n", name[i]);
}
}
printf(" \n");
}
持续侦听用户信息
我们的服务器应该是一直处于监听状态的,为了达到这个目的我们可以写个 while
语句或者和我一样铤而走险写个 goto
语句。
我们大概的框架如下:
flag1:
cc = recv(sock, temp, BUFLEN, 0); //cc为接收的字符数
if(收到的是离线信息) {
Do Somthing......
}
else if(收到的是正常信息) {
Do Somthing......
goto flag1;
}
下面我们将逐一补全用户离线以及正常信息处理这两个模块。
用户离线及广播
我们通过建立连接可以很容易地判断一个用户上线,那我们如何判断一个用户离线呢?这里有个技巧,我们不是一直在侦听用户通过 socket 发过来的字符串嘛,我们可以用一个变量 $cc$ 来记录发送来的字符串的长度,如果 cc == SOCKET_ERROR || cc == 0
我们就可以大胆地判断出这个用户已经断开连接了,这时我们只要向全体在线的用户广播该用户下线的信息就可以了。
所以我们的程序实现如下:
cc = recv(sock, temp, BUFLEN, 0); //cc为接收的字符数
if(cc == SOCKET_ERROR|| cc == 0) {
(void) time(&now);
pts = ctime(&now);
sprintf(buf2, "客户<%s> 离开了聊天室 \n", name[nownumber]);
sock = NULL;
sockets[number] = NULL;
CloseHandle(hThread[number]);
printf("%s ", buf2);
printf("\t将自动把此数据发送给所有客户\n");
for(int i = 0; i <= number; i++) {
if(sockets[i] != NULL && sockets[i] != sock) {
(void) send(sockets[i], buf2, sizeof(buf2), 0);
printf(" 发送至用户<%s>成功\n", name[i]);
}
}
printf(" \n");
}
正常信息的处理
那我们如何判断一条信息是正常信息呢?其实由于后面你会看到,我们在用户端进行了约束,只有符合我们上述信息格式的信息才能发送到我们服务端,所以我们只需判断信息长度 cc > 0
就好啦。接下来我们按照收到的信息类型不同分别进行处理,所以大体的框架如下:
flag1:
cc = recv(sock, temp, BUFLEN, 0); //cc为接收的字符数
if(cc == SOCKET_ERROR|| cc == 0) {
Do Something......
}
else if(cc > 0) {
(void) time(&now);
pts = ctime(&now);
int len = strlen(temp);
if(temp[0] == '@') {
Do Something......
}
else if(temp[0] == '?'){
Do Something......
}
else if(temp[0] == '*') {
Do Something......
}
else if(temp[0] == '+') {
Do Something......
}
else if(temp[0] == '-') {
Do Something......
}
goto flag1;
}
转发信息
作为一个聊天服务器,最重要的莫过于转发信息了。由于我们服务器和用户是呈现“一个中心”的设计原则,所以我们的用户想发送信息给指定的对象,都需要先发送到我们的聊天服务器,再由我们的聊天服务器代为转发(这样还能监控有没有非法信息)。那我们的服务器究竟是如何实现的呢?
首先我们要分情况,看看是发送给个人的信息(即以 $@$ 开头),还是发送到群组的信息(即以 $@@$ 开头)。我们先来看看发送给个人的信息如何处理。如果是发送给个人的代码,那么信息的第二位就不为 $@$,此时我们的信息结构就是 @name:message
那我们的第一步理所应该的就是用一个循环先把我们的 $name$ 和 $message$ 分离出来分别存储在字符数组 $target$ 和 $buf3$ 当中。
接着我们判断是否是发送给所有人的,如果是,那么此时 $name == ALL$ 我们就枚举所有在线的并且不是发送这条信息的用户的用户,将这条信息和发送者信息打包发送过去。如果不是,那么我们就在在线的用户中,找到名叫 $name$ 的用户,然后把这条信息转发给他。
所以发送给个人的代码就如下:
flag1:
cc = recv(sock, temp, BUFLEN, 0); //cc为接收的字符数
if(cc == SOCKET_ERROR|| cc == 0) {
Do Something......
}
else if(cc > 0) {
(void) time(&now);
pts = ctime(&now);
int len = strlen(temp);
if(temp[0] == '@') {
if(temp[1] == '@') {
Do Something......
}
else {
int flag = -1;
for(int i = 0; i < len; i++) {
if(temp[i] == ':') {
flag = i;
break;
}
}
for(int i = 1; i < flag; i++) target[i - 1] = temp[i]; target[flag] = '\0';
for(int i = flag + 1; i <= len; i++) buf3[i - flag - 1] = temp[i];
sprintf(buf4, "用户<%s>说 :%s \n \t\t时间 : %s", name[nownumber], buf3, pts);
if(!strcmp(target ,"ALL")) {
printf("%s ", buf4);
printf("\t将自动把此数据发送给所有客户 \n");
for(int i = 0; i <= number; i++) {
if(sockets[i] != NULL && sockets[i] != sock) {
(void) send(sockets[i],buf4, sizeof(buf4), 0);
printf(" 发送至用户<%s>成功\n", name[i]);
}
}
printf(" \n");
}
else {
printf("%s ", buf4);
printf("\t将自动把此数据发送给用户<%s> \n", target);
for(int i = 0; i <= number; i++) {
if(sockets[i] != NULL && sockets[i] != sock && !strcmp(target, name[i])) {
(void) send(sockets[i],buf4, sizeof(buf4), 0);
printf(" 发送至用户<%s>成功\n", name[i]);
}
}
printf(" \n");
}
}
}
else if(temp[0] == '?'){
Do Something......
}
else if(temp[0] == '*') {
Do Something......
}
else if(temp[0] == '+') {
Do Something......
}
else if(temp[0] == '-') {
Do Something......
}
goto flag1;
}
下面我们再来看看如何处理发送给一个群组的。其实这部分给发送给个人的程序是高度一致的,首先 $@@$ 开头表示是发送给群组的消息,那么消息结构就是@@group:message
那我们的第一步还是先把我们的 $group$ 和 $message$ 分离出来分别存储在字符数组 $target$ 和 $buf3$ 当中。接着我们先枚举一下已有的群组,看看是否存在这个群组,如果存在我们再枚举所有用户,将信息、发送者和来源群组的信息打包好转发给在这个群组的所有用户。
所以转发给群组的代码就如下:
flag1:cc = recv(sock, temp, BUFLEN, 0); //cc为接收的字符数if(cc == SOCKET_ERROR|| cc == 0) { Do Something...... }else if(cc > 0) { (void) time(&now); pts = ctime(&now); int len = strlen(temp); if(temp[0] == '@') { if(temp[1] == '@') { int flag = -1; for(int i = 2; i < len; i++) { if(temp[i] == ':') { flag = i; break; } } for(int i = 2; i < flag; i++) target[i - 2] = temp[i]; target[flag] = '\0'; for(int i = flag + 1; i <= len; i++) buf3[i - flag - 1] = temp[i]; sprintf(buf4, "用户<%s>在群组<%s>中说 :%s \n \t\t时间 : %s", name[nownumber], target, buf3, pts); flag = -1; for(int i = 0; i <= number; i++) { if(sockets[i] != NULL) { if(!strcmp(target, group[i]) && s[i].size()) { flag = i; break; } } } if(flag == -1) { sprintf(buf4, "群聊<%s>已解散或不存在,信息发送失败", target); (void) send(sock, buf4, sizeof(buf4), 0); } else { printf("%s ", buf4); printf("\t将自动把此数据发送给群组<%s>! \n", target); for(int i = 0; i <= number; i++) { if(sockets[i] != NULL && sockets[i] != sock) { if(s[flag].find(i) != s[flag].end()) { (void) send(sockets[i],buf4, sizeof(buf4), 0); printf(" 发送至群组<%s>中用户<%s>成功!\n", target, name[i]); } } } printf(" \n"); } } else { Do Something...... } } else if(temp[0] == '?'){ Do Something...... } else if(temp[0] == '*') { Do Something...... } else if(temp[0] == '+') { Do Something...... } else if(temp[0] == '-') { Do Something...... } goto flag1;}
到这里,我们转发信息的工作就完成了。
查询呼叫
在这一步中,我们要帮助用户查询好友是否在线并且给在线的这个用户发送“你的朋友正在找你”的信息。这个实现比较简单,我们还是先把信息结构当中 ?name
的 $name$ 分离出来,存储在字符数组 $target$ 中,这样我们就只要遍历所有用户,看看要查找的用户是否存在以及是否在线就可以了。所以这部分的代码非常简单如下所示。
flag1:cc = recv(sock, temp, BUFLEN, 0); //cc为接收的字符数if(cc == SOCKET_ERROR|| cc == 0) { Do Something......}else if(cc > 0) { (void) time(&now); pts = ctime(&now); int len = strlen(temp); if(temp[0] == '@') { Do Something...... } else if(temp[0] == '?'){ for(int i = 1; i <= len; i++) target[i - 1] = temp[i]; sprintf(buf4, "用户<%s>正在找你呢 \t\t时间 : %s", name[nownumber], pts); int flag = 0; for(int i = 0; i <= number; i++) { if(sockets[i] != NULL && sockets[i] != sock && !strcmp(target, name[i])) { (void) send(sockets[i],buf4, sizeof(buf4), 0); printf(" 发送至用户<%s>成功!\n", name[i]); flag = 1; } } if(flag) { sprintf(buf4, "你寻找的用户<%s>在线哦", target); (void) send(sock, buf4, sizeof(buf4), 0); } else { sprintf(buf4, "你寻找的用户<%s>离线哦", target); (void) send(sock, buf4, sizeof(buf4), 0); } } else if(temp[0] == '*') { Do Something...... } else if(temp[0] == '+') { Do Something...... } else if(temp[0] == '-') { Do Something...... } goto flag1;}
管理群聊
管理群聊是很有意思的一部分,我们管理群聊分为创建群聊,加入群聊和退出群聊三部分,我们肯定还是要先把要操作的群组名分离出来存储到 target
当中。首先是创建群聊,其指令是*group
,我们先要判断创建群组的这个用户之前有没有未解散的群组,这个可以用之前群组成员集合是否为空来判断。如果存在,那么就先解散之前的群组,即把之前群组成员集合清空,然后再创建新的群组,把创建者加入到该群组的成员列表中。
然后是添加群组。我们首先先遍历所有群组列表,看看是否存在这个群组,如果存在这个群组我们再看看这个用户是否已经在这个群组中了,只有他不在这个群组中,我们才可以把他加入到这个群组的成员列表中,并返回添加成功的信息。
最后是退出群聊。我们首先还是先遍历所有群组列表,看看是否存在这个群组,如果存在这个群组我们再看看这个用户是否已经在这个群组中了,如果不在返回退出失败。如果在这个群聊中,还要判断一下这个用户是否是这个群聊的创建者,如果是就要直接解散这个群聊反之直接退出就好啦。
所以这一部分的代码如下:
flag1:cc = recv(sock, temp, BUFLEN, 0); //cc为接收的字符数if(cc == SOCKET_ERROR|| cc == 0) { Do Something......}else if(cc > 0) { (void) time(&now); pts = ctime(&now); int len = strlen(temp); if(temp[0] == '@') { Do Something...... } else if(temp[0] == '?'){ Do Something...... } else if(temp[0] == '*') { for(int i = 1; i <= len; i++) target[i - 1] = temp[i]; if(!s[nownumber].size()) { sprintf(buf4, "你的新群聊<%s>已成功创建", target); strcpy(group[nownumber], target); s[nownumber].clear(); s[nownumber].insert(nownumber); (void) send(sock, buf4, sizeof(buf4), 0); } else { sprintf(buf4, "你的原群聊<%s>已解散,群聊<%s>已成功创建", group[nownumber], target); strcpy(group[nownumber], target); s[nownumber].clear(); s[nownumber].insert(nownumber); (void) send(sock, buf4, sizeof(buf4), 0); } } else if(temp[0] == '+') { int flag = -1; for(int i = 1; i <= len; i++) target[i - 1] = temp[i]; for(int i = 0; i <= number; i++) { if(sockets[i] != NULL) { if(!strcmp(target, group[i]) && s[i].size()) { flag = i; break; } } } if(flag == -1) { sprintf(buf4, "群聊<%s>已解散或不存在,加入失败", target); (void) send(sock, buf4, sizeof(buf4), 0); } else { if(s[flag].find(nownumber) != s[flag].end()) { sprintf(buf4, "你已经在群聊<%s>当中了,加入失败", target); (void) send(sock, buf4, sizeof(buf4), 0); } else { sprintf(buf4, "你已成功加入群聊<%s>", target); (void) send(sock, buf4, sizeof(buf4), 0); s[flag].insert(nownumber) ; } } } else if(temp[0] == '-') { int flag = -1; for(int i = 1; i <= len; i++) target[i - 1] = temp[i]; for(int i = 0; i <= number; i++) { if(sockets[i] != NULL) { if(!strcmp(target, group[i]) && s[i].size()) { flag = i; break; } } } if(flag == -1) { sprintf(buf4, "群聊<%s>已解散或不存在,退出失败", target); (void) send(sock, buf4, sizeof(buf4), 0); } else { if(flag == nownumber) { sprintf(buf4, "群聊<%s>解散成功", target); s[nownumber].clear(); (void) send(sock, buf4, sizeof(buf4), 0); } else { if(s[flag].find(nownumber) != s[flag].end()) { sprintf(buf4, "群聊<%s>退出成功", target); s[flag].erase(nownumber); (void) send(sock, buf4, sizeof(buf4), 0); } else { sprintf(buf4, "你不在群聊<%s>中,退出失败", target); (void) send(sock, buf4, sizeof(buf4), 0); } } } } } goto flag1;}
用户端
连接服务器
我们首先要与服务器取得连接。由于我们是在一台电脑上进行通信,所以肯定也少不了多线程,所以我们在主程序要先建立一个$socket$,然后开一个线程接受服务器发来的信息,保持通信。多线程上要做的事情,我们就用一个函数 $chat$ 来实现。剩下的就是给用户输出一些提示信息什么的了,这都是次要的。所以这个是主程序建立连接的部分:
sock = socket(PF_INET, SOCK_STREAM, 0);connect(sock, (struct sockaddr*)&sin, sizeof(sin));printf("\t\t\t\tChat 多人聊天程序 \n");printf("\t\t\t\t (Client) \n");hThread = (HANDLE)_beginthreadex(NULL, 0, Chat, NULL, 0, &threadID); printf(" \t\t\t\t 【您可以自由发言】\n\n");
这个函数和我们服务器的 $chat$ 类似,都是建立在一个线程上用于通信。我们在这个函数当中,我们也是不断接受服务器传来的信息,如果传来的信息长度 $cc$ 满足 cc == SOCKET_ERROR|| cc == 0
我们就认为已经和服务器断开了连接,就返回错误信息,退出循环就可以了;不然的话我们就在屏幕上输出我们接受到的信息。所以子函数的实现如下:
unsigned int __stdcall Chat(PVOID PM) { time_t now; (void) time(&now); pts = ctime(&now); char buf[2000]; while(1) { int cc = recv(sock, buf, BUFLEN, 0); //cc为接收的字符数 if(cc == SOCKET_ERROR|| cc == 0) { printf("Error: %d.----",GetLastError()); printf("与服务器断开连接!\n"); CloseHandle(hThread); (void)closesocket(sock); break; } else if(cc > 0) { // buf[cc] = '\0'; printf("%s\n",buf); // printf("输入数据(exit退出): \n"); } } return 0;}
发送昵称
在建立连接以后,我们就要向服务器发送昵称,这一步很有意思。由于与服务器建立通信是需要时间的,所以太早发送昵称可能会使得服务器无法收到你的昵称。为了解决这个问题,我们引入了 $Sleep()$ 函数,顾名思义就是先让我们的程序停一下,等我们的连接建立完成后,再发送我们的昵称就能保证我们的服务器收到了。这一部分的实现如下:
char buf1[2000];Sleep(500);printf("正在连接到服务器,请稍后……\n");Sleep(2000);printf("请输入你的聊天昵称:\n"); gets(buf1);(void) send(sock, buf1, sizeof(buf1), 0);(void) time(&now);pts = ctime(&now);printf(" 昵称发送成功! ------时间: %s\n",pts);
文本检查和通信
为了化简,我们在服务器端不会检查我们的信息是否合法,所以让信息合法的重任就留到了客户端这边。那么如何判断信息合法呢?也非常简单,我们就看看是否符合我们上述的信息格式就好了,如果信息合法直接发送给服务器不合法的话就让客户再次输入,就不过多赘述了。所以这一部分的代码如下所示:
while(1) { // scanf("%s",&buf1); gets(buf1); if(!strcmp(buf1 ,"exit")) goto end; int len = strlen(buf1); int flag = -1; for(int i = 0; i < len; i++) { if(buf1[i] == ':') { flag = i; break; } } while(!((flag != -1 && buf1[0] == '@') || buf1[0] == '?' || buf1[0] == '*' || buf1[0] == '+' || buf1[0] == '-')) { printf("格式错误,请重新输入!\n"); gets(buf1); if(!strcmp(buf1 ,"exit")) goto end; int len = strlen(buf1); flag = -1; for(int i = 0; i < len; i++) { if(buf1[i] == ':') { flag = i; break; } } } (void) send(sock, buf1, sizeof(buf1), 0); (void) time(&now); pts = ctime(&now); printf(" 发送成功! ------时间: %s\n",pts);}end:
代码展示
所以总的服务器端代码就是:
/* TCPdtd.cpp - main, TCPdaytimed */ #include <stdlib.h>#include <stdio.h>#include <winsock2.h>#include <time.h>#include "conio.h"#include <windows.h>#include <process.h>#include <math.h>#include <set> #define QLEN 5#define WSVERS MAKEWORD(2, 0)#define BUFLEN 2000 // 缓冲区大小#pragma comment(lib,"ws2_32.lib") //winsock 2.2 libraryusing namespace std;SOCKET msock, ssock; //master & slave sockets SOCKET sockets[100] = {NULL};char name[100][2000];char group[100][2000];set<int> s[2000];int cc;char *pts; //pointer to time string time_t now; //current time char buf[2000]; //buffer char *input; HANDLE hThread1, hThread[100] = {NULL};unsigned int threadID, ThreadID[100], number; struct sockaddr_in fsin;struct sockaddr_in Sin; unsigned int __stdcall Chat(PVOID PM) { int nownumber = number; char buf1[2000]; char buf2[2000]; char buf3[2000]; char buf4[2000]; char temp[2000]; char target[2000]; (void) time(&now); pts = ctime(&now); sockets[number] = ssock; SOCKET sock = ssock; ThreadID[number] = threadID; unsigned int threadid = threadID; sprintf(buf1, " 时间: %s \t【我的线程号: %d 】\n", pts, threadid); (void) send(sock, buf1, sizeof(buf1), 0); recv(sock, buf3, BUFLEN, 0); //cc为接收的字符数 if(true) {//恒执行 strcpy(name[nownumber], buf3); sprintf(buf2, "%s,欢迎加入聊天室", name[nownumber]); (void) send(sock, buf3, sizeof(buf3), 0); sprintf(buf2, "客户<%s> 加入了聊天室 \n", name[nownumber]); printf("%s ", buf2); printf("\t将自动把此数据发送给所有客户 \n"); for(int i = 0; i <= number; i++) { if(sockets[i] != NULL && sockets[i] != sock) { (void) send(sockets[i],buf2, sizeof(buf2), 0); printf(" 发送至用户<%s>成功\n", name[i]); } } printf(" \n"); } flag1: cc = recv(sock, temp, BUFLEN, 0); //cc为接收的字符数 if(cc == SOCKET_ERROR|| cc == 0) { (void) time(&now); pts = ctime(&now); sprintf(buf2, "客户<%s> 离开了聊天室 \n", name[nownumber]); sock = NULL; sockets[number] = NULL; CloseHandle(hThread[number]); printf("%s ", buf2); printf("\t将自动把此数据发送给所有客户\n"); for(int i = 0; i <= number; i++) { if(sockets[i] != NULL && sockets[i] != sock) { (void) send(sockets[i], buf2, sizeof(buf2), 0); printf(" 发送至用户<%s>成功\n", name[i]); } } printf(" \n"); } else if(cc > 0) { (void) time(&now); pts = ctime(&now); int len = strlen(temp); if(temp[0] == '@') { if(temp[1] == '@') { int flag = -1; for(int i = 2; i < len; i++) { if(temp[i] == ':') { flag = i; break; } } for(int i = 2; i < flag; i++) target[i - 2] = temp[i]; target[flag] = '\0'; for(int i = flag + 1; i <= len; i++) buf3[i - flag - 1] = temp[i]; sprintf(buf4, "用户<%s>在群组<%s>中说 :%s \n \t\t时间 : %s", name[nownumber], target, buf3, pts); flag = -1; for(int i = 0; i <= number; i++) { if(sockets[i] != NULL) { if(!strcmp(target, group[i]) && s[i].size()) { flag = i; break; } } } if(flag == -1) { sprintf(buf4, "群聊<%s>已解散或不存在,信息发送失败", target); (void) send(sock, buf4, sizeof(buf4), 0); } else { printf("%s ", buf4); printf("\t将自动把此数据发送给群组<%s>! \n", target); for(int i = 0; i <= number; i++) { if(sockets[i] != NULL && sockets[i] != sock) { if(s[flag].find(i) != s[flag].end()) { (void) send(sockets[i],buf4, sizeof(buf4), 0); printf(" 发送至群组<%s>中用户<%s>成功!\n", target, name[i]); } } } printf(" \n"); } } else { int flag = -1; for(int i = 0; i < len; i++) { if(temp[i] == ':') { flag = i; break; } } for(int i = 1; i < flag; i++) target[i - 1] = temp[i]; target[flag] = '\0'; for(int i = flag + 1; i <= len; i++) buf3[i - flag - 1] = temp[i]; sprintf(buf4, "用户<%s>说 :%s \n \t\t时间 : %s", name[nownumber], buf3, pts); if(!strcmp(target ,"ALL")) { printf("%s ", buf4); printf("\t将自动把此数据发送给所有客户 \n"); for(int i = 0; i <= number; i++) { if(sockets[i] != NULL && sockets[i] != sock) { (void) send(sockets[i],buf4, sizeof(buf4), 0); printf(" 发送至用户<%s>成功\n", name[i]); } } printf(" \n"); } else { printf("%s ", buf4); printf("\t将自动把此数据发送给用户<%s> \n", target); for(int i = 0; i <= number; i++) { if(sockets[i] != NULL && sockets[i] != sock && !strcmp(target, name[i])) { (void) send(sockets[i],buf4, sizeof(buf4), 0); printf(" 发送至用户<%s>成功\n", name[i]); } } printf(" \n"); } } } else if(temp[0] == '?'){ for(int i = 1; i <= len; i++) target[i - 1] = temp[i]; sprintf(buf4, "用户<%s>正在找你呢 \t\t时间 : %s", name[nownumber], pts); int flag = 0; for(int i = 0; i <= number; i++) { if(sockets[i] != NULL && sockets[i] != sock && !strcmp(target, name[i])) { (void) send(sockets[i],buf4, sizeof(buf4), 0); printf(" 发送至用户<%s>成功!\n", name[i]); flag = 1; } } if(flag) { sprintf(buf4, "你寻找的用户<%s>在线哦", target); (void) send(sock, buf4, sizeof(buf4), 0); } else { sprintf(buf4, "你寻找的用户<%s>离线哦", target); (void) send(sock, buf4, sizeof(buf4), 0); } } else if(temp[0] == '*') { for(int i = 1; i <= len; i++) target[i - 1] = temp[i]; if(!s[nownumber].size()) { sprintf(buf4, "你的新群聊<%s>已成功创建", target); strcpy(group[nownumber], target); s[nownumber].clear(); s[nownumber].insert(nownumber); (void) send(sock, buf4, sizeof(buf4), 0); } else { sprintf(buf4, "你的原群聊<%s>已解散,群聊<%s>已成功创建", group[nownumber], target); strcpy(group[nownumber], target); s[nownumber].clear(); s[nownumber].insert(nownumber); (void) send(sock, buf4, sizeof(buf4), 0); } } else if(temp[0] == '+') { int flag = -1; for(int i = 1; i <= len; i++) target[i - 1] = temp[i]; for(int i = 0; i <= number; i++) { if(sockets[i] != NULL) { if(!strcmp(target, group[i]) && s[i].size()) { flag = i; break; } } } if(flag == -1) { sprintf(buf4, "群聊<%s>已解散或不存在,加入失败", target); (void) send(sock, buf4, sizeof(buf4), 0); } else { if(s[flag].find(nownumber) != s[flag].end()) { sprintf(buf4, "你已经在群聊<%s>当中了,加入失败", target); (void) send(sock, buf4, sizeof(buf4), 0); } else { sprintf(buf4, "你已成功加入群聊<%s>", target); (void) send(sock, buf4, sizeof(buf4), 0); s[flag].insert(nownumber) ; } } } else if(temp[0] == '-') { int flag = -1; for(int i = 1; i <= len; i++) target[i - 1] = temp[i]; for(int i = 0; i <= number; i++) { if(sockets[i] != NULL) { if(!strcmp(target, group[i]) && s[i].size()) { flag = i; break; } } } if(flag == -1) { sprintf(buf4, "群聊<%s>已解散或不存在,退出失败", target); (void) send(sock, buf4, sizeof(buf4), 0); } else { if(flag == nownumber) { sprintf(buf4, "群聊<%s>解散成功", target); s[nownumber].clear(); (void) send(sock, buf4, sizeof(buf4), 0); } else { if(s[flag].find(nownumber) != s[flag].end()) { sprintf(buf4, "群聊<%s>退出成功", target); s[flag].erase(nownumber); (void) send(sock, buf4, sizeof(buf4), 0); } else { sprintf(buf4, "你不在群聊<%s>中,退出失败", target); (void) send(sock, buf4, sizeof(buf4), 0); } } } } goto flag1; } (void) closesocket(sock); return 0;} /*------------------------------------------------------------------------ * main - Iterative TCP server for DAYTIME service *------------------------------------------------------------------------ */int main(int argc, char *argv[]) {/* argc: 命令行参数个数, 例如:C:\> TCPdaytimed 8080 argc=2 argv[0]="TCPdaytimed",argv[1]="8080" */ int alen; /* from-address length */ WSADATA wsadata; char service[] = "5050"; WSAStartup(WSVERS, &wsadata); //加载 winsock 2.2 library msock = socket(PF_INET, SOCK_STREAM, 0); //生成套接字。TCP协议号=6, UDP协议号=17 memset(&Sin, 0, sizeof(Sin)); Sin.sin_family = AF_INET; Sin.sin_addr.s_addr = INADDR_ANY; //指定绑定接口的IP地址。INADDR_ANY表示绑定(监听)所有的接口。 Sin.sin_port = htons((u_short)atoi(service)); //atoi--把ascii转化为int,htons - 主机序(host)转化为网络序(network), s(short) bind(msock, (struct sockaddr *)&Sin, sizeof(Sin)); // 绑定端口号(和IP地址) listen(msock, 5); //队列长度为5 printf("\t\t\t\t Chat 多人聊天程序 \n"); printf("\t\t\t\t (Server) \n"); (void) time(&now); pts = ctime(&now); printf("\t\t\t 时间 :%s", pts); number = -1; while(1) { //检测是否有按键 alen = sizeof(struct sockaddr); ssock = accept(msock, (struct sockaddr *)&fsin, &alen); number++; hThread[number] = (HANDLE)_beginthreadex(NULL, 0, Chat, NULL, 0, &threadID); } (void) closesocket(msock); WSACleanup(); //卸载载 winsock 2.2 library return 0;}
客户端的代码就是:
/* TCPClient.cpp -- 用于传递struct */#include <stdlib.h>#include <stdio.h>#include <winsock2.h>#include <string.h>#include <time.h>#include <windows.h>#include <process.h>#include <math.h> #define BUFLEN 2000 // 缓冲区大小#define WSVERS MAKEWORD(2, 0) // 指明版本2.0 #pragma comment(lib,"ws2_32.lib") // 指明winsock 2.0 Llibrary /*------------------------------------------------------------------------ * main - TCP client for DAYTIME service *------------------------------------------------------------------------ */ SOCKET sock,sockets[100] = {NULL}; // socket descriptor // int cc; // recv character count char *packet = NULL; // buffer for one line of text char *pts,*input;HANDLE hThread;unsigned threadID; unsigned int __stdcall Chat(PVOID PM) { time_t now; (void) time(&now); pts = ctime(&now); char buf[2000]; while(1) { int cc = recv(sock, buf, BUFLEN, 0); //cc为接收的字符数 if(cc == SOCKET_ERROR|| cc == 0) { printf("Error: %d.----",GetLastError()); printf("与服务器断开连接!\n"); CloseHandle(hThread); (void)closesocket(sock); break; } else if(cc > 0) { // buf[cc] = '\0'; printf("%s\n",buf); // printf("输入数据(exit退出): \n"); } } return 0;} int main(int argc, char *argv[]){ time_t now; (void) time(&now); pts = ctime(&now); char host[] = "127.0.0.1"; /* server IP to connect */ char service[] = "5050"; /* server port to connect */ struct sockaddr_in sin; /* an Internet endpoint address */ WSADATA wsadata; WSAStartup(WSVERS, &wsadata); /* 启动某版本Socket的DLL */ memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons((u_short)atoi(service)); //atoi:把ascii转化为int. htons:主机序(host)转化为网络序(network), s--short sin.sin_addr.s_addr = inet_addr(host); //如果host为域名,需要先用函数gethostbyname把域名转化为IP地址 sock = socket(PF_INET, SOCK_STREAM, 0); connect(sock, (struct sockaddr*)&sin, sizeof(sin)); printf("\t\t\t\tChat 多人聊天程序 \n"); printf("\t\t\t\t (Client) \n"); hThread = (HANDLE)_beginthreadex(NULL, 0, Chat, NULL, 0, &threadID); printf(" \t\t\t\t 【您可以自由发言】\n\n"); char buf1[2000]; Sleep(500); printf("正在连接到服务器,请稍后……\n"); Sleep(2000); printf("请输入你的聊天昵称:\n"); gets(buf1); (void) send(sock, buf1, sizeof(buf1), 0); (void) time(&now); pts = ctime(&now); printf(" 昵称发送成功! ------时间: %s\n",pts); while(1) { // scanf("%s",&buf1); gets(buf1); if(!strcmp(buf1 ,"exit")) goto end; int len = strlen(buf1); int flag = -1; for(int i = 0; i < len; i++) { if(buf1[i] == ':') { flag = i; break; } } while(!((flag != -1 && buf1[0] == '@') || buf1[0] == '?' || buf1[0] == '*' || buf1[0] == '+' || buf1[0] == '-')) { printf("格式错误,请重新输入!\n"); gets(buf1); if(!strcmp(buf1 ,"exit")) goto end; int len = strlen(buf1); flag = -1; for(int i = 0; i < len; i++) { if(buf1[i] == ':') { flag = i; break; } } } (void) send(sock, buf1, sizeof(buf1), 0); (void) time(&now); pts = ctime(&now); printf(" 发送成功! ------时间: %s\n",pts); } end: CloseHandle(hThread); closesocket(sock); WSACleanup(); /* 卸载某版本的DLL */ printf("按回车键继续..."); getchar(); return 0; /* exit */}
效果展示
登录功能
我们打开服务器端和一个用户端,可以看到我们的客户端与服务器端已经建立了连接,确定了线程号,现在需要输入用户名(昵称)来登录上聊天服务器:
我们在用户端输入 $QwQ$,可以看到服务端已经接受了请求,创建了这个用户:
通知功能
我们再登录两个用户 $QAQ$ 和 $QuQ$,可以看到我们收到了登录的通知:
我们接下来退出 $QwQ$,我们可以看到收到了退出通知(期间还有一个没有输入昵称的用户上线并下线):
聊天功能
我们用 $QwQ$ 向所有人发送 $hello$, 可以看到所有人都能正常收到信息:
这时我们用 $QAQ$ 给 $QwQ$ 发送 $yes$,我们看到只有 $QwQ$ 收到了这个信息:
呼叫功能
我们这时用 $QuQ$ 查找 $QwQ$ 的在线情况,可以看到正常返回了在线情况,并且 $QwQ$ 也收到了建立通信的请求。
群聊功能
我们先用 $QwQ$ 建立一个叫做 $233$ 的群组:
然后 $QAQ$ 和 $QuQ$ 加入群组:
然后$QuQ$ 退出群组:
然后$QAQ$ 在群组里说 $hi$,可以看到只有还在群组里的 $QwQ$ 收到了这条信息:
可以看到我们的程序能正常通信并且界面还算比较友好。
Wireshark包分析
由于我们的 $socket$ 包是在本地通信的,不经过网卡,所以一般的 $wireshark$ 是无法截取到包的.现在一个比较好的解决方案是用$npcap$,这个工具是北大的一个博士,叫做 $Luo yang$,开发的。源码放在https://github.com/nmap/npcap,还在持续维护中。这个工具的原理大概就是虚拟了一个网卡让操作系统把 $loopback$ 的数据镜像一份到 $npcap adapter$,然后 $wireshark$ 可以通过截取这个 $npcap $ $adatper$上的数据包来获得对本地数据包进行分析的一个途径。大家不要小看了这个东西,在$npcap$ $adatper$上,$wireshark$抓到的可是本地的所有数据包,这个不得了,用处大了,特别是对于需要深入了解各种库的工作原理的。它可以抓取各种本地上的包,下载地址在https://nmap.org/npcap/#download。
下载后按照我的图片进行配置安装就可以了:
安装完之后,我们打开 $wireshark$ 就可以看到,我们多了一块网卡($Npcap$ $loopback$ $Adapter$):
这时我们就可以进行正常的抓包了。
我们首先登录服务器,开启客户端,可以看到建立TCP连接的过程:
接下来我们的客户端发送昵称 $QwQ$ 给服务器,也被我们抓取到了:
接下来我们添加两个用户 $QAQ$ 和 $QuQ$,我们用 $QwQ$ 群发 $hello$ 给所有用户,可以看到我们用户发给服务器以及服务器转发给另外两个用户的包传输过程:
其他的过程也是类似的,在这里就不意义展示了。
总结与不足
在这次的实验中,我们完成了TCP连接下的多人聊天实验室,完成了书上的任务目标,并寻找方法完成了包的捕获和分析,按照既定目标完成了实验。但是实验中还是有些许遗憾由于时间问题没有解决,比如用户的注册注销账号以及账号密码的管理,再比如可视化的交互界面和高效的文件传输,更有甚者还想尝试在公网上的部署。我将在日后的学习中不断完善开发这套系统,也希望最后能完成一个真正完善可用的网络在线聊天室。
One comment