C++跨平台开发——关于解决SOCKET网络编程中客户端对聊的问题

一、多进程实现

        前面我们在SOCKET网络编程学习中,简单实现了服务器为客户端单独开一个进程,客户端网络连接成功之后可以输入数据,并陆续给服务器发送消息,服务器只实现读取操作。

基于VS2019 C++的跨平台(Linux)开发(2.2)——SOCKET网络编程

        因为之前服务器中没有实现write,所以只能实现客户端发消息给服务器。那么如果想要让客户端和客户端进行聊天该怎么办?

        服务器作为中转站,要实现客户对聊,就要把服务器保存的消息再发送给另一方的客户端。(如下图)利用write写出去,并且writefd是5

        但是此时会出现这样的问题:每一个客户端是一个进程,比如客户端A有一个acceptfd为4,当客户端B上线之后,才有第二个acceptfd为5,会把上一次的acceptfd覆盖(因为声明的acceptfd变量就一个)。

        因为开的是进程,即此时acceptfd=4的进程要给acceptfd=5的进程发消息(acceptfd=5的进程要给acceptfd=4的进程发消息),但是拿不到两个进程都获取不到对方的acceptfd ,因为进程之前数据不能共享。(不能定义全局变量进行加减操作获取acceptfd,就算可以,也不能保证另一个客户端acceptfd=5,就像你登录QQ的时候,腾讯公司不会告诉你是今天第几个上线的用户,但是登录QQ要使用唯一的QQ号,服务器只能通过QQ号识别客户端)

        所以,通过id号和acceptfd就可以一一对应了,就像map中的键值对,这时候大家可能会想到定义一个全局map容器把两者存起来,但是依然逃不掉进程间不能共享全局变量问题,所以很遗憾还是解决不了问题。

        综上,使用进程代表一个客户端本身就是不对的,用了进程就不得不用IPC,这样都会变得更加复杂。所以就得使用多线程技术。


二、多线程实现 

        所以,客户端上线之后服务器要调用pthread_create创建线程为他服务,然后要去处理acceptfd的读写操作。思路如下

        首先,要使用map将用户ID和acceptfd绑定。因为客户端上线只有acceptfd,还不知道用户的ID,实现不了将acceptfd和账号进行绑定。所以要在线程的处理函数进行死循环,再用read读取acceptfd,并查询数据库获取账号密码,判断账号密码是否正确,只有验证登录成功后才用map保存下来并插入数据库。(即map保存要在read后操作)。所以在pthread_create的时候要传入acceptfd。

        其次,read后要write(acceptfd)。那么只有明确了“我是谁、跟谁聊、聊什么”才能告诉服务器要跟谁聊天,这时就可以定义一个结构体(如下图)—— 根据业务自定义通信协议。然后根据接收者账号(recvID),在map容器查询对应的acceptfd,然后即可以write(acceptfd)了。

 注意:

1、调用线程处理函数没必要传map,本来就可以支持全局变量的使用,可以直接访问。

2、不能将acceptfd定义为全局变量,因为服务器调用accept后返回acceptfd会一直把全局的acceptfd覆盖,导致所有的线程只为最后一次上线的客户端服务


三、案例

四、详细步骤

服务端逻辑

总框架:接收客户端请求——>处理请求操作——>返回业务处理结果(使用的通信协议如下)

  • 1、read读客户端请求,如果是登录,则做登录验证
  • 2、成功或者失败结果write给客服端fd
  • 3、read读客户端请求,如果是聊天,则确定1-发送者,2-接收者,3-聊天内容
  • 3-1、从在线用户map容器中找到聊天对象
  • 3-2、给原发送者发一次write,给聊天对象发一次write

客户端逻辑

  • 1、登录操作,write发送客户端自己的账号密码
  • 2、read接收读取的登录结果(失败就重新登录)
  • 3、如果成功则开始聊天,失败继续登录
  • 3-1、write数据:1-发送者;2-接收者;3-聊天内容

注意点:

1、void*传参,函数体赋值参数

  • int fd=* (int *)p; ——赋值的是值,进入线程执行一次
  • int* fd= (int *)p;——赋值的是地址  ,不能使用

如果赋值地址,保存到map容器,后续上线一个客户端,地址内容就改变了。会出现第一次可以发送成功,但是之后每次都是自己给自己发送消息。如下图,第二个客户端上线之后开辟的是子线程2号,它的pfd的指向的地址还是原来的0x19ff2c,pfd的值表示5,子线程1号的pfd也被改成了5,因此绝对不可以使用指针

2、为什么服务器还要给原发送者发一次write呢?

因为网络的数据传输需要时间,如果只给接收者发送,可能导致数据不能同一时间到达, 就如客户端1发了10句“你好”花了1毫秒,而在客户端2中就收到了一句花了一分钟,导致不能正常聊天。如下图。如果服务器也给原发送者发一次,那么数据就基本会同时到达(只是时间问题)

3、完整的通信协议=通信协议头+通信协议体

服务端要read两次,第一次先read头,第二次再根据读到的类型和长度来执行业务。

//通信协议头
typedef struct protocolHead {
	int businessTyep;//业务类型  登录1 聊天2
	int businessLen;//业务长度 登录40字节 聊天140 字节
}HEAD;
//通信协议体==业务数据
typedef struct login
{

	char userName[20];
	char pwd[20];
}LOGIN;
typedef struct chatMsg {
	char sendID[10];
	char recID[10];
	char msg[100];
}CHAT;
//示例
//...
Head h;
LOGIN l;
CHAT ch;
read(fd, &h, sizeof(HEAD));//读头,确定业务
if (h.businessTyep == 1)//读体,处理业务
{
	read(fd,&l,h.businessLen);

}
else if (h.businessTyep == 2)
{
	read(fd, &msg, h.businessLen);
}

注意:

        处理业务时,读取的是协议头的长度businessLen,而不是直接sizeof(Head),是因为发送的数据有可能是空的,而使用businessLen才是真正的业务长度,就可以解决read登录、read聊天

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ze言

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值