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客户端。如下所示:
此时再看一号客户端和二号客户端都被阻塞,一号客户端阻塞的原因是因为没有收到消息而被read函数阻塞,此时服务端实际上是直接将消息回复给了二号客户端,并且二号客户端此时已经收到了来自于服务端“错发”的消息,此时之所以还被阻塞,是因为现在getchar正处于等待输入的状态,如果我们此时从二号客户端发送一条消息,就可以立即显示出服务端刚刚“错发”的消息:
此时二号客户端显示的实际上就是前面一号客户端发送给服务端,却被服务端误认为是二号客户端发来的,然后直接回复给了二号客户端。如果接下来二号客户端继续向服务端发消息,实际上也会收到相应的服务端发回的消息,只不过此时消息的显示就出现了一种“延迟”的现象。
为了避免这种问题,我后来又采取了使用局部临时变量来存放每次连接更新后的connskt值,这样实际上也是不行的:因为局部变量位于栈上,每次循环结束这个局部变量从当前位置弹出,再次进入循环时又会压入当前位置,因此每次循环中的局部变量地址实际上是不会改变的。
为了解决这个问题,可以直接使用数组,数组中的每个元素都用来存放一次连接的信息,由于数组中的元素之间地址肯定是不同的,因此各自使用的描述符也是完全不同的,这样才不会出现问题;还有一种方法就是在堆上动态开辟内存用来存放每次连接的信息,这样也是可以的,不过需要注意的是,这种情况就要考虑内存泄漏的问题了。
运行结果