先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题
一 why
一般地,socket server端会对接多个client,在server端需要支持连接多个client,并进行数据交互,在《linux进程间通信—本地socket套接字(二)—多进程实现一个server对应多个client》中,我们采样了多进程法来实现。其实,我们也可以采用多线程法来实现
二 what
那么,我们如何利用多线程实现一个server对接多个client呢?我们知道,每次server接收到client的连接请求是通过accept函数实现的,这个函数返回值为client的文件描述符,因此每次server接收到一个client的连接请求,就创建一个子线程,用于和这个client建立数据交互,如下如所示
实现原理如下:
1. server端有一个主线程,只用于接收client端的连接请求,每接收到一次连接请求,就创建一个子线程,这个子线程用来实现和client的数据交互。
2. 子线程用来实现和client端进行数据交互。
三 how
server.c代码
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/shm.h>
#define PORT 8890
#define QUEUE_SIZE 10
#define BUFFER_SIZE 1024
void *do_communication(void *arg)
{
char buf[BUFFER_SIZE] = {0};
int cfd = *(int *)arg;
int recvlen;
while (1) {
memset(buf, 0, sizeof(buf));
recvlen = read(cfd, buf, sizeof(buf));
if (recvlen < 0) {
perror("recv fail");
break;
} else if (recvlen ==0) {
printf("client[%d] exitn", cfd);
break;
} else {
printf("now server recv : %sn", buf);
write(cfd, buf, recvlen);
}
}
close(cfd);
return NULL;
}
int main(int argc, char **argv)
{
struct sockaddr_in server_sockaddr, client_addr;
socklen_t length = sizeof(client_addr);
char str[16];
pthread_t pid;
int server_sockfd, cfd;
int ret = 0;
int reuse = 1;
//定义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));
ret = pthread_create(&pid, NULL, do_communication, (void *)&cfd);
if (ret < 0) {
perror("pthread_create fail");
return -1;
}
pthread_detach(pid); //线程回收,线程结束之后自动回收
}
printf("closed.n");
close(server_sockfd);
return 0;
}
client.c代码
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#define SERVER_PORT 8890
#define BUFFER_SIZE 1024
#define CLIENT_IP_ADDR "127.0.0.1"
int main(int argc, char **argv)
{
struct sockaddr_in servaddr;
char sendbuf[BUFFER_SIZE] = {0};
char recvbuf[BUFFER_SIZE] = {0};
char str[16];
int client_fd;
//定义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 : %sn", 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,"exitn")==0)
{
printf("client exited.n");
break;
}
recv(client_fd, recvbuf, sizeof(recvbuf),0); ///接收
printf("client receive: %sn", recvbuf);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(client_fd);
return 0;
}
四 test
编译,因为需要使用pthread_create,所以编译时需要指定参数-lpthread。
再起一个client,可以发现server端检测到两个client,两者的port端口号不一样。
经过测试,我们发现一个bug,现象是:
- 启动server
- 启动client1和client2
- server和client1,2数据传输
- ctrl+c关闭server(注意我们先关闭了server)
- ctrl+c关闭client
- 再次重新启动server,发现提示
在分析这个问题之前,先插入一个知识,TCP传输分层结构入戏下:
我们在server中使用bind函数,将ip地址和port端口号关联在一起,使用通配符地址(INADDR_ANY),它允许任何接口为到来的连接所使用。
但是使用bind绑定ip地址和port端口号时,可能绑定一个已经存在的端口号,虽然此时不存在活动的socket,但是由于socket存在的TIME_WAIT机制,该端口号状态在套接字关闭后约保留 2 到 4 分钟。在 TIME_WAIT 状态退出之后,套接字被删除,该地址才能被重新绑定而不出问题。
等待 TIME_WAIT 结束是一件令人恼火的事,特别是如果您正在开发一个套接字服务器,就需要停止服务器来做一些改动,然后重启。幸运的是,有方法可以避开 TIME_WAIT 状态。可以给套接字应用 SO_REUSEADDR 套接字选项,以便端口可以马上重用。新的server端程序如下:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/shm.h>
#define SERVER_PORT 8890
#define QUEUE_SIZE 10
#define BUFFER_SIZE 1024
void *do_communication(void *arg)
{
char buf[BUFFER_SIZE] = {0};
int cfd = *(int *)arg;
int recvlen;
while (1) {
memset(buf, 0, sizeof(buf));
recvlen = read(cfd, buf, sizeof(buf));
if (recvlen < 0) {
perror("recv fail");
break;
} else if (recvlen ==0) {
printf("client[%d] exitn", cfd);
break;
} else {
printf("now server recv : %sn", buf);
write(cfd, buf, recvlen);
}
}
close(cfd);
return NULL;
}
int main(int argc, char **argv)
{
struct sockaddr_in server_sockaddr, client_addr;
socklen_t length = sizeof(client_addr);
char str[16];
pthread_t pid;
int server_sockfd, cfd;
int ret = 0;
int reuse = 1;
//定义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 erroen");
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); //线程回收,线程结束之后自动回收
}
printf("closed.n");
close(server_sockfd);
return 0;
}
请关注,如下代码片段,这段代码就是实现了重新使用port端口号。这样设置只是为了实现我们方便快速的调试代码,正式版的server,我们不建议这么做。
//使能可以重新使用addr
ret = setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR,
&reuse, sizeof(reuse));
if (ret < 0) {
perror("setsockopt erroen");
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.