在最开始实现并发服务端的时候, 最开始想到的办法便是使用多进程, 使每个 TCP 连接对应一个进程; 之后我们又将多进程并发改写成了 IO 复用的方式实现了相同的功能; 但现在唯一没有将服务端改写成线程, 本节就来改写服务端.
线程服务端
在改写成多线程之前, 要对 [线程创建], [线程同步]以及线程终止有所了解才行.
主函数仅仅只是将连接后回射操作交给线程即可 :
int main(int argc, char *argv[]){
int sockfd, clientfd;
socklen_t socklen;
struct sockaddr_in servAddr, cliAddr;
pthread_t tid;
bzero(&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(atoi(argv[1]));
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
socklen = sizeof(cliAddr);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
goto exit;
if(bind(sockfd, (struct sockaddr* )&servAddr, sizeof(servAddr)) < 0)
goto exit;
listen(sockfd, 5);
while(1){
clientfd = accept(sockfd, (struct sockaddr *)&cliAddr, &socklen);
if(clientfd < 0)
goto exit;
// 将回射功能交给线程
if(pthread_create(&tid, NULL, echo, (void *)&clientfd))
goto exit;
}
exit:
return 0;
}
线程函数就只是一个简单的回射功能 :
void *echo(void *sockfd){
// 使线程分离, 退出后直接释放资源
pthread_detach(pthread_self());
char buf[BUFSIZE];
int n;
int fd = *(int *)sockfd;
while(1){
n = read(fd, buf, sizeof(buf));
if(0 == n) break;
write(fd, buf, n);
}
close(fd);
pthread_exit((void *)0);
}
线程函数中注意需要执行 pthread_detach()
函数来完成线程分离, 这样主进程就可以不执行 pthread_join
阻塞来回收线程的资源, 线程结束后自己回收资源.
服务端完整代码 : service.c
服务端 :
./service 8080
客户端完整代码 : client.c
客服端 :
./client 127.0.0.1 8080
线程回射程序bug
在前面不管是多进程还是IO复用我们都或多或少遇到过问题, 那么改写成线程也同样会有意想不到的问题.
上面的回射程序有一处很严重的bug.
pthread_create(&tid, NULL, echo, (void *)&clientfd)
我们函数参数传入的是指针, 线程是并发执行, 对于参数 clientfd
就容易导致同步问题. 如果为每次的clientfd
申请内存, 也会导致函数并非线程安全, 并且频繁的申请也容易导致申请失败同时内存释放也是一个问题.
解决方法 :
- 加锁, 保证同步. 但是使用锁运行效率肯定就很低, 反而性能会降低.
- 封装函数传入形参或者结构体 . 一般都采用该呢方式, 也有更好的封装和扩展性. 之后可以在线程池[1]中看到.
小结
- 线程编程注意同步问题.
- 可以将 web 客户端 的改写成线程, 这样就可以不将 connect 设置成非阻塞.