Linux/C++ 实现多线程服务端Socket通信(线程共享套接字无法正常通信问题分析与解决)

目录

程序流程

程序实例

 值得注意的地方

运行结果


多进程服务器处理连接类似,本例的目的是在主线程中创建多个子线程来处理连接请求。

程序流程

1. 创建基本的套接字,并绑定地址信息、设置监听;

2. 创建一个线程函数,用于子线程中进行客户端与服务端的数据通信;

3. 循环accept来接收连接请求,每接收一个连接请求,就创建一个子线程;

4. 将子线程detach(),使子线程在结束后自行释放内存。

程序实例

#include<iostream>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<errno.h>
#include<pthread.h>
#include<sys/socket.h>
#include<sys/wait.h>

using namespace std;

#define SERV_IP "127.1.2.3"
#define SERV_PORT 8888
#define MAX_CONN 100  

struct socket_info
{
	struct sockaddr_in sktaddr;
	int fd;
};

void *connfun(void *arg)  //线程入口,处理客户端与服务端的通信
{
	socket_info *connskt = (socket_info *)arg;  //参数强制转换
	cout<<inet_ntoa((*connskt).sktaddr.sin_addr)<<":"<<ntohs((*connskt).sktaddr.sin_port)<<" connected ...  "<<endl;
	while(1)
	{
		char buf[1024];
		
		int readstate = read(connskt->fd,buf,sizeof(buf));
		
		if(readstate == -1)  
		{
			cout<<"read error : "<<strerror(errno)<<endl;
			break;		
		}
		else if(readstate == 0)  //客户端退出
		{
			cout<<inet_ntoa((*connskt).sktaddr.sin_addr)<<":"<<ntohs((*connskt).sktaddr.sin_port)<<" exit ... "<<endl;
			break;
		}
		write(STDOUT_FILENO,buf,readstate);  //打印收到的数据
		cout<<"      (From "<<inet_ntoa((*connskt).sktaddr.sin_addr)<<":"<<ntohs((*connskt).sktaddr.sin_port)<<")"<<endl;
		for(int i=0;i<readstate;i++)buf[i] = toupper(buf[i]); //字母转为大写
		write(connskt->fd,buf,readstate);  //发回客户端	
	}
	close(connskt->fd);  //关闭当前连接所使用的文件描述符
	return NULL;
}

int main()
{
	socket_info listenskt,connskt;
	pthread_t tid;
	socklen_t clit_size ;
	socket_info soc[MAX_CONN];   //用来存放每个连接的信息
	int i=0;
	
	memset(&listenskt,0,sizeof(listenskt));
	memset(&connskt,0,sizeof(connskt));

	if((listenskt.fd = socket(AF_INET,SOCK_STREAM,0)) == -1)
	{
		cout<<"socket create failed : "<<strerror(errno)<<endl;
		return 0;
	}

	listenskt.sktaddr.sin_family = AF_INET;
	listenskt.sktaddr.sin_port = htons(SERV_PORT);
	listenskt.sktaddr.sin_addr.s_addr = inet_addr(SERV_IP);

	if(bind(listenskt.fd,(sockaddr *)&listenskt.sktaddr,sizeof(listenskt.sktaddr)) == -1 )
	{
		cout<<"bind error : "<<strerror(errno)<<endl;
		return 0;
	}

	if(listen(listenskt.fd,100) == -1)
	{
		cout<<"listen error : "<<strerror(errno)<<endl;
		return 0;	
	}
 		
	cout<<"Init Success ! "<<endl;
	cout<<"host ip : "<<inet_ntoa(listenskt.sktaddr.sin_addr)<<"  port : "<<ntohs(listenskt.sktaddr.sin_port)<<endl;

	cout<<"Waiting for connections ... "<<endl;

	while(1)
	{
		clit_size = sizeof(connskt.sktaddr);
		if( (connskt.fd = accept(listenskt.fd,(sockaddr *)&connskt.sktaddr,&clit_size) ) == -1 )
		{
			if(errno == EINTR)continue ;  //必须加上这一句,当一个客户端断开时会引发中断异常,也就是这里的EINTR,通过这句话来重新accept,不然就会直接退出
			cout<<"accept error : "<<strerror(errno)<<endl;
			return 0;
		}
		memcpy(&soc[i],&connskt,sizeof(connskt)); //将新连接的信息拷贝到soc数组中

		pthread_create(&tid,NULL,connfun,(void *)&soc[i++]);
		pthread_detach(tid);

		if(i>=MAX_CONN)  //如果连接数过多,就不再接收连接请求了
		{
			cout<<"Reach the max connections : "<<strerror(errno)<<endl;
			break;
		}
	}
	
	close(listenskt.fd);
	return 0;

}

 值得注意的地方

       这里有个需要注意的地方,就是这里使用了一个soc数组用来存放每一个客户端与服务端的连接,每当完成一次成功连接,就会将connskt.fd更新一遍,然后soc数组就将更新后的connskt存到数组中作为一个元素,并将该元素作为线程函数的参数传入。为什么要这么做呢?比如说我在第一次写的时候主线程中while是这样写的:

        while(1)
	{
		clit_size = sizeof(connskt.sktaddr);
		if( (connskt.fd = accept(listenskt.fd,(sockaddr *)&connskt.sktaddr,&clit_size) ) == -1 )
		{
			if(errno == EINTR)continue ;  //必须加上这一句,当一个客户端断开时会引发中断异常,也就是这里的EINTR,通过这句话来重新accept,不然主线程就会直接退出,就无法接收其他请求了
			cout<<"accept error : "<<strerror(errno)<<endl;
			return 0;
		}
                //创建线程
		pthread_create(&tid,NULL,connfun,(void *)&connskt);
		pthread_detach(tid);
	}

       这样实际运行起来后,是会出现问题的:假设先用客户端A连接服务端,此时A可以与服务端进行正常的收发;然后再用客户端B连接服务端,此时B也可以与服务端进行正常的收发;然后再用客户端C与服务端进行连接,C与服务端的通信也是正常的。但是,如果此时再通过客户端A或者客户端B向服务端发送消息,就收不到服务端回发的消息了,只有用C与服务端进行数据收发是正常的。这是为什么呢?

        造成这个问题的原因就是,线程不同于进程,这里传入的connskt地址对于子线程来说都是共享的,因此所有子线程都会从传入的地址单元中获取连接的文件描述符,当A第一次连接服务端时,这个文件描述符包含着A与服务端的连接信息,然后当B连接时,这个文件描述符就更新成了包含B与服务端的连接信息,然后到C也是如此,此时的文件描述符所包含的信息实际上是C与服务端的连接信息了。如果此时再通过B向服务端发送消息,实际上收到消息的是服务端与C连接所在的子线程,然后服务端写回消息实际上也是写回给的C客户端。如下所示:

Linux/C++ 实现多线程服务端Socket通信(线程共享套接字无法正常通信问题分析与解决)

        此时再看一号客户端和二号客户端都被阻塞,一号客户端阻塞的原因是因为没有收到消息而被read函数阻塞,此时服务端实际上是直接将消息回复给了二号客户端,并且二号客户端此时已经收到了来自于服务端“错发”的消息,此时之所以还被阻塞,是因为现在getchar正处于等待输入的状态,如果我们此时从二号客户端发送一条消息,就可以立即显示出服务端刚刚“错发”的消息:

Linux/C++ 实现多线程服务端Socket通信(线程共享套接字无法正常通信问题分析与解决)

        此时二号客户端显示的实际上就是前面一号客户端发送给服务端,却被服务端误认为是二号客户端发来的,然后直接回复给了二号客户端。如果接下来二号客户端继续向服务端发消息,实际上也会收到相应的服务端发回的消息,只不过此时消息的显示就出现了一种“延迟”的现象。

        为了避免这种问题,我后来又采取了使用局部临时变量来存放每次连接更新后的connskt值,这样实际上也是不行的:因为局部变量位于栈上,每次循环结束这个局部变量从当前位置弹出,再次进入循环时又会压入当前位置,因此每次循环中的局部变量地址实际上是不会改变的。
        为了解决这个问题,可以直接使用数组,数组中的每个元素都用来存放一次连接的信息,由于数组中的元素之间地址肯定是不同的,因此各自使用的描述符也是完全不同的,这样才不会出现问题;还有一种方法就是在堆上动态开辟内存用来存放每次连接的信息,这样也是可以的,不过需要注意的是,这种情况就要考虑内存泄漏的问题了。

运行结果

Linux/C++ 实现多线程服务端Socket通信(线程共享套接字无法正常通信问题分析与解决)