C++跨平台开发——SOCKET网络编程中实现客户端对聊

一、案例

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));
				
			}
		}


	}
}

四、运行效果

客户端: 

服务器: 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ze言

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

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

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

打赏作者

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

抵扣说明:

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

余额充值