先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题。想要获取完整源码的,关注公众号后回复“socket3”即可。
一 why
一般地,socket server端会对接多个client,在server端需要支持连接多个client,并进行数据交互,在《linux进程间通信—本地socket套接字(二)—多进程实现一个server对应多个client》中,我们采样了多进程法来实现。其实,同样地,我们也可以采用多线程法来实现。
二 what
那么,我们如何利用多线程实现一个server对接多个client呢?我们知道,每次server接收到client的连接请求是通过accept函数实现的,这个函数返回值为client的文件描述符,因此每次server接收到一个client的连接请求,就创建一个子线程,用于和这个client建立数据交互,如下如所示
实现原理如下:
- server端有一个主线程,只用于接收client端的连接请求,每接收到一次连接请求,就创建一个子线程,这个子线程用来实现和client的数据交互
- 子线程用来实现和client端进行数据交互
三 how
server.c代码框架,想获取完整源码的,请关注公众号:嵌入式Linux江湖,回复关键字“socket3”即可
/* 通信线程 */
void *do_communication(void *arg)
{
......
while (1) {
memset(buf, 0, sizeof(buf));
recvlen = read(cfd, buf, sizeof(buf));
......
}
......
}
int main(int argc, char **argv)
{
......
//定义IPV4的TCP连接的套接字描述符
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sockfd < 0) {
perror("socket() fail!\n");
return -1;
}
//定义sockaddr_in
memset(&server_sockaddr, 0, sizeof(server_sockaddr));
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
server_sockaddr.sin_port = htons(PORT);
//bind成功返回0,出错返回-1
ret = bind(server_sockfd, (struct sockaddr *)&server_sockaddr,
sizeof(server_sockaddr));
if(ret < 0) {
perror("bind");
return -1;//1为异常退出
}
printf("bind success.\n");
//listen成功返回0,出错返回-1,允许同时帧听的连接数为QUEUE_SIZE
ret = listen(server_sockfd, QUEUE_SIZE);
if(ret < 0) {
perror("listen");
return -1;
}
printf("listen success.\n");
while(1) {
//进程阻塞在accept上,成功返回非负描述字,出错返回-1
cfd = accept(server_sockfd, (struct sockaddr*)&client_addr,&length);
if(cfd < 0) {
perror("connect");
return -1;
}
printf("new client accepted, ip : %s, port : %d.\n",
inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
ntohs(client_addr.sin_port));
/* 创建子线程处理通信
*/
}
......
}
client.c代码
int main(int argc, char **argv)
{
......
//定义IPV4的TCP连接的套接字描述符
client_fd = socket(AF_INET,SOCK_STREAM, 0);
//set sockaddr_in
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr(CLIENT_IP_ADDR);
servaddr.sin_port = htons(SERVER_PORT); //服务器端口
printf("ip addr : %s\n", CLIENT_IP_ADDR);
//连接服务器,成功返回0,错误返回-1
if (connect(client_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
perror("connect");
exit(1);
}
printf("connect server(IP:%s).\n",
inet_ntop(AF_INET, &servaddr.sin_addr, str, sizeof(str)));
//客户端将控制台输入的信息发送给服务器端,服务器原样返回信息
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
send(client_fd, sendbuf, strlen(sendbuf),0); ///发送
if(strcmp(sendbuf,"exit\n")==0)
{
printf("client exited.\n");
break;
}
recv(client_fd, recvbuf, sizeof(recvbuf),0); ///接收
printf("client receive: %s\n", recvbuf);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
......
}
四 test
编译,因为需要使用pthread_create,所以编译时需要制定参数-lpthread
gcc 5_server.c -o 5_server -lpthread
gcc 5_client.c -o 5_client
运行server并先起一个client
再起一个client,可以发现server端检测到两个client,两者的port端口号不一样
经过测试,我们发现一个bug,现象是:
- 启动server
- 启动client1和client2
- server和client1,2数据传输
- ctrl+c关闭server(注意我们先关闭了server)
- ctrl+c关闭client
- 再次重新启动server,发现提示
在分析这个问题之前,先插入一个知识,TCP传输分层结构
按照上面的结构,当我们先退出server,在退出client之后,
这是因为我们在server中使用bind函数,将ip地址和port端口号关联在一起,使用通配符地址(INADDR_ANY),它允许任何接口为到来的连接所使用。
但是使用bind绑定ip地址和port端口号时,可能存在绑定一个已经存在的端口号,虽然此时不存在活动的socket,但是由于socket存在的TIME_WAIT机制,该端口号状态在套接字关闭后约保留 2 到 4 分钟。在 TIME_WAIT 状态退出之后,套接字被删除,该地址才能被重新绑定而不出问题。
等待 TIME_WAIT 结束是一件令人恼火的事,特别是如果您正在开发一个套接字服务器,就需要停止服务器来做一些改动,然后重启。幸运的是,有方法可以避开 TIME_WAIT 状态。可以给套接字应用 SO_REUSEADDR 套接字选项,以便端口可以马上重用。新的server端程序如下:
void *do_communication(void *arg)
{
......
while (1) {
memset(buf, 0, sizeof(buf));
recvlen = read(cfd, buf, sizeof(buf));
......
}
......
}
int main(int argc, char **argv)
{
......
//定义IPV4的TCP连接的套接字描述符
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sockfd < 0) {
perror("socket() fail!\n");
return -1;
}
//使能可以重新使用addr
ret = setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR,
&reuse, sizeof(reuse));
if (ret < 0) {
perror("setsockopt erroe\n");
return -1;
}
//定义sockaddr_in
memset(&server_sockaddr, 0, sizeof(server_sockaddr));
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
server_sockaddr.sin_port = htons(SERVER_PORT);
//bind成功返回0,出错返回-1
ret = bind(server_sockfd, (struct sockaddr *)&server_sockaddr,
sizeof(server_sockaddr));
if(ret < 0) {
perror("bind");
return -1;//1为异常退出
}
printf("bind success.\n");
//listen成功返回0,出错返回-1,允许同时帧听的连接数为QUEUE_SIZE
ret = listen(server_sockfd, QUEUE_SIZE);
if(ret < 0) {
perror("listen");
return -1;
}
printf("listen success.\n");
while(1) {
//进程阻塞在accept上,成功返回非负描述字,出错返回-1
cfd = accept(server_sockfd, (struct sockaddr*)&client_addr,&length);
if(cfd < 0) {
perror("connect");
return -1;
}
printf("new client accepted, client_ip : %s, client_port : %d.\n",
inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
ntohs(client_addr.sin_port));
ret = pthread_create(&pid, NULL, do_communication, (void *)&cfd);
if (ret < 0) {
perror("pthread_create fail");
return -1;
}
pthread_detach(pid); //线程回收,线程结束之后自动回收
}
......
}
请关注,如下代码片段,这段代码就是实现了重新使用port端口号。这样设置只是为了实现我们方便快速的调试代码,正式版的server,我们不建议这么做。
//使能可以重新使用addr
ret = setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR,
&reuse, sizeof(reuse));
if (ret < 0) {
perror("setsockopt erroe\n");
return -1;
}
另外,我们考虑另一个问题,如果仍然是之前的代码,我们先ctrl+c退出客户端,然后在退出服务端,会不会出现同样的问题呢?神奇的是,竟然没有发生这种情况,这又是为什么呢?
退出之前,使用netstat -apn | grep 8890
查看client和server的socket状态如下,
A表示server listen状态,B表示server和client已经建立连接状态。C表示client和server已经建立连接状态
当我们先关闭client,然后在关闭server时候,但是client已经发送信号告诉了server,所以实际上server这个时候已经处于close转台了。两个client是处于TIME_WAIT状态,但是由于client的端口号是自动分配的,所以我们下次再启动server,然后再启动client,就不会出现bind: address already in use的状态。
但是,如果我们先关闭server,然后再关闭client,因为此时server发送了FIN信号之后,没有等到client回复ACK信号,所以会处在一个TIME_WAIT状态中,如下
请注意最下面的server的状态,这个时候的server是处在TIME_WAIT,还没有处在close状态,这个时候,如果我们仍然用bind去绑定一个ip地址和端口号时,就会出现bind: address already in use.