一、案例
C++跨平台开发——关于解决SOCKET网络编程中客户端对聊的问题
二、实现分析
1、为什么服务器开启线程而不是进程?
线程的开销小,启动快,共享数据(不需要ipc就可以实现交互),所以一个线程一个客户端(实际上效率提升将近10倍)。
2、创建线程的时候传accepfd的原因
accepfd记录客户端,客户端每上线一个就覆盖一个accepfd,第一个accepfd就没了,利用值传参并保存下来。线程永远都停留在死循环内,保存下来的fd就不会被替换掉。(知道客户端下线)
3、自定义通信协议
创新头文件写入请求头和请求体
4、客户端如何发送数据
错误1:
客户端连接上后在while循环中直接发出请求、接收数据(write 、 read)
错误分析:
read(阻塞函数)用来接收服务器返回来的结果,但如果服务器处理某个业务要花费很长时间,网络很慢、数据很长,read就不能及时返回,卡了半小时还没有回复。就会导致write发送完请求,然后进行read没有收到结果,就不能继续write(write 、 read相互制约)
write 完不一定就必须立刻read(就好像打游戏的时候你挂机(没有write),但是依然会接收到服务器发回来的界面数据)
所以对于客户端来说就需要让write 和read同时存在,并且同时运行——创建读、写两个线程并进行传参
注意:
创建线程不能写在死循环里面,但如果没有死循环也是错的。
如果加上循环变成循环开启读写线程,造成电脑卡死;如果不在死循环内,创建线程完直接走到return ,导致main中的子进程结束,因为线程包含在进程当中(共享进程开辟的cpu、内存等资源),线程自然也消失了。(即进程没了线程就没了)——再次验证了进程是资源分配的基本单位,线程共享进程的所有资源。
所以客户端连接上必须开启死循环
技巧:一旦到了线程执行函数就开启死循环
写线程函数体部分:结构体在内存中以什么样的形式存放?
以字节的形式存放。(如果是字符,只能存char类型,没有什么字符可以表示结构体),字节的长度用sizeof求得,如果用strlen就会导致客户端收不到服务器发送的消息
结构体还遵循内存对齐的原则,连续存放
怎么把两个结构体(用户、请求头)一起往外发?
创建一个buf数组来动态获取结构体的长度(不定长,根据业务情况)
注意:
以下这样调用memcpy会将前面的数据进行覆盖,要利用指针的偏移计算
指针的偏移计算:
前面拷贝的是一个head结构体,buf从首元素开始,偏移整个head,将之前拷贝的数据跨过去,找到没有数据的那个地址再进行拷贝user(如图)
使得能正好存下两个结构体的大小,节省空
注意:
没有必要再创建一个结构体放请求头和请求体,因为不同的业务有不同的长度,导致长度不能确定,如果你按照最大的业务长度来计算,而这些业务你又可能用不到,就会浪费空间(就像你打开qq客户端,不能保证你会用到里面所有的业务,可能就只进行聊天或者说开了100k的空间只存放了50字节)
5、服务器如何接收数据?
读的时候先读请求头
错误2:

错误分析:
上图中类型和长度分别对应 1、 40是正确的。但是后面一堆数据是有问题的,因为在服务器这边仅仅只接收了请求头, 后面的数据还没有接收到就一直死循环打印
注意:
Socket通信中,存放的数据被读走就没了,没被读走的那部分还是保留在Socket中,那么这个时候还会触发read,就打印了出来。所以接下来判断是什么业务的时候还要继续读Socket剩下部分(用户信息)
接着读多少长度是根据业务长度,而不是直接利用sizeof(USERINFO),仅有当创建的缓冲区跟sizeof(USERINFO)一样大的情况下才不会出现问题,否则哪怕就丢了一个字节也不行。
一方面,保证接收的刚刚好,另一方面,按照业务长度进行验证(安全),发多少收多少,还可以根据read的返回值判断接收是否成功,如果数据丢了,返回的res一定比给定长度小,如果发多了(粘包),也不会接收多余没用的数据(告诉我长度20个字节,却发了25个字节,后面5个字节不会接收,服从客户端的要求)。
服务器判断如果进行登录业务的话,就进行验证并加入在线用户的容器
发送结果——反馈是否操作成功——客户端添加并读取反馈
写线程怎么知道读线程的数据?
聊天之前要先判断登录是否成功(客户端读线程),登录成功才开始聊天(客户端写线程)——设置全局变量进行判断
接着定义聊天结构体,定义请求头,并拷贝数据
技巧:
在写read和write的时候一定要记得带上返回值,利用返回值来查看,至少可以避免一端的问题。
处理业务时,读取完数据后先进行判断,可以先做打印测试
服务器这边的聊天业务怎么处理?
Map容器怎么通过key来查找值?
1、Find 2、[]
错误3:
错误分析:
图中服务器端打印的onlineMap[sev_chat.recvID] = 0,即查不到要接受者的id,所以map容器第一个参数不能定义成char*指针类型(地址),因为服务器操作针对所有的客户端,地址就可能会改变(或者数据变),应修改为string类型
补充:
1、TCP存在问题(数据问题)
- 可能丢包
- 可能粘包
- 可能半包
2、Linux中怎么同时编译多个文件的方法(.h .cpp)
- 编写makefile
- 直接执行vs2019的bin目录下的可执行文件.out
三、核心代码
//客户端
int main() {
pthread_t read_thrad;
pthread_t write_thrad;
struct sockaddr_in s_addr;
int socketfd = 0;
int length = 0;
int acceptfd = 0;//客户端的文件描述符
char cli_buf[120] = { 0 };
//初始化网络
socketfd = socket(AF_INET, SOCK_STREAM, 0);//AF_INET表示使用ipv4,SOCK_STREAM表示流式套接字
if (socketfd == -1)
{
perror(" socket error");
}
else
{
//确定使用那个协议族 ipv4
s_addr.sin_family = AF_INET;
//连接服务器的地址
s_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址的转换
s_addr.sin_port = htons(10086);
length = sizeof(s_addr);
//准备通道不做绑定操作而是直接连接
if (connect(socketfd, (struct sockaddr*)&s_addr, length) == -1)
{
perror(" connect error");
}
else
{
cout << "客户端连接成功 acceptfd = " << endl;
//进入线程执行函数要开启死循环
if (pthread_create(&read_thrad, NULL, read_thread_func, &socketfd))
{
perror("pthread_create error");
}
if (pthread_create(&write_thrad, NULL, write_thread_func, &socketfd))
{
perror("pthread_create error");
}
//为了不让线程马上结束
while (true)
{
}
}
}
return 0;
}
void* read_thread_func(void* p)
{
BACK resback;
CHARMSG resmsg;
int rfd = *(int*)p;
while (true)
{
read(rfd,&resback,sizeof(BACK));
cout << "resback.businessType" << resback.businessType << endl;
cout << "resback.flag" << resback.flag << endl;
if (resback.businessType == 1)
{
if (resback.flag == 1)
{
isRun = 1;
}
else
{
isRun = 0;
}
}
else if (resback.businessType == 2)//聊天
{
int res = read(rfd, &resmsg, sizeof(CHARMSG));
cout << "chat content" << resmsg.content << endl;
}
}
}
//写线程执行函数
void* write_thread_func(void* p)
{
//char buf[1024] = { 0 };
HEAD myhead;
USERINFO puser;
CHARMSG pchat;
int wfd = *(int*)p;
cout << "请输入账号" << endl;
cin >> puser.userName;
cout << "请输入密码" << endl;
cin >> puser.pwd;
myhead.businessType = 1;//业务类型
myhead.businessLen = sizeof(USERINFO);
//根据业务情况动态获取结构体长度
//结构体对应字节,不能用strlen
char buf[sizeof(HEAD)+sizeof(USERINFO)] = { 0 };
memcpy(buf,&myhead,sizeof(HEAD));
memcpy(buf+sizeof(HEAD),&puser,sizeof(USERINFO));
write(wfd,buf,sizeof(buf));
//做聊天用
while (true)
{
if (isRun == 1)
{
cout << "登录成功 进行聊天" << endl;
cout << "请输入账号" << endl;
cin >> pchat.sendID;
cout << "请输入聊天对象" << endl;
cin >> pchat.recvID;
cout << "请输入聊天内容" << endl;
cin >> pchat.content;
bzero(&myhead,sizeof(myhead));
myhead.businessType = 2;
myhead.businessLen = sizeof(CHARMSG);
//这里的buf不能使用前面的
char chat_buf[sizeof(HEAD) + sizeof(CHARMSG)] = { 0 };
memcpy(chat_buf, &myhead, sizeof(HEAD));
memcpy(chat_buf + sizeof(HEAD), &pchat, sizeof(CHARMSG));
int res = write(wfd, chat_buf, sizeof(chat_buf));
if (res > 0)
{
cout << "-------------------聊天信息发送成功-------------------" << endl;
}
}
}
}
int main() {
struct sockaddr_in s_addr;
int socketfd = 0;
int length = 0;
int acceptfd = 0;//客户端的文件描述符
char ser_buf[66] = { 0 };
int pid = 0;
//初始化网络
socketfd = socket(AF_INET,SOCK_STREAM,0);
if (socketfd == -1)
{
perror(" socket error");
}
else
{
//确定使用那个协议族 ipv4
s_addr.sin_family = AF_INET;
//系统自动获取本机ip地址
s_addr.sin_addr.s_addr = INADDR_ANY;
//端口65535,10000以下是操作系统使用,自己定义需要10000以后
s_addr.sin_port = htons(10086);
length = sizeof(s_addr);
//绑定ip地址和端口号
if (bind(socketfd,(struct sockaddr*)&s_addr,length) == -1)
{
perror(" bind error");
}
//监听这个地址和端口有没有客户端连接
if (listen(socketfd,10) == -1)
{
perror(" listen error");
}
cout << "服务器网络通道准备好了" << endl;
//死循环保证服务器长时间在线
pthread_t thread_id;
while (true)
{
cout << "等待客户端上线" << endl;
//等待客户端上线,地址和端口号已经设置过了,所以为null,如果没有客户端访问则一直被动等待
//返回值就表示那个客户端(给客户端发消息不需要知道客户端的ip地址)
acceptfd = accept(socketfd, NULL, NULL);//阻塞函数
cout << "客户端连接成功 acceptfd = " << acceptfd << endl;
if (pthread_create(&thread_id, NULL, pthread_func, &acceptfd))
{
perror("pthread_create error");
}
}
}
return 0;
}
//服务器
//1、接收客户端请求,并为客户端服务,2、处理客户端请求 3、返回业务处理结果(发送数据)
void* pthread_func(void* p)
{
HEAD h;
USERINFO uinfo;
CHARMSG sev_chat;
int acptfd = *(int*)p;//赋值的是值,进入线程执行一次。
//int *pfd = (int *)p;//赋值的是地址,觉得不可以使用指针
while (true)//两个while同时跑
{
read(acptfd,&h,sizeof(HEAD));
cout << "h.businessType = " << h.businessType << endl;
cout << "h.businessLen = " << h.businessLen << endl;
if (h.businessType == 1)
{
int res = read(acptfd,&uinfo, h.businessLen);
if (res == h.businessLen)
{
cout << ".userName = " << uinfo.userName << endl;
cout << ".pwd = " << uinfo.pwd << endl;
//进行验证并加入在线用户的容器
onlineMap[uinfo.userName] = acptfd;
cout << ".onlineUser = " <<onlineMap.size()<< endl;
//发送结果——通知是否成功——客户端添加反馈
BACK back;
back.businessLen = 0;
back.businessType = 1;
back.flag = 1;//表示操作成功
char buf[sizeof(BACK)] = { 0 };
memcpy(buf,&back,sizeof(BACK));
write(acptfd,buf,sizeof(buf));
}
}
else if (h.businessType == 2)//处理聊天业务
{
int res = read(acptfd,&sev_chat,h.businessLen);
if (res == h.businessLen)
{
cout << "sev_chat.sendID = " << sev_chat.sendID << endl;
cout << "sev_chat.recvID = " << sev_chat.recvID << endl;
cout << "sev_chat.content = " << sev_chat.content << endl;
cout << "长度 = " << sizeof(sev_chat.recvID) << endl;
cout << "onlineMap[sev_chat.recvID] = " << onlineMap[sev_chat.recvID] << endl;
char chat_buf[sizeof(BACK) + sizeof(CHARMSG)] = { 0 };
BACK fbk;
fbk.businessLen = sizeof(sev_chat);
fbk.businessType = 2;
fbk.flag = 1;//表示操作成功
memcpy(chat_buf, &fbk, sizeof(BACK));
memcpy(chat_buf + sizeof(BACK), &sev_chat, sizeof(sev_chat));
write(acptfd,&chat_buf, sizeof(chat_buf));
write(onlineMap[sev_chat.recvID], &chat_buf, sizeof(chat_buf));
}
}
}
}
四、运行效果
客户端:
服务器: