Socket多线程
OVERVIEW
fix1
将Socket多进程实现的聊天程序修改为多线程实现:
- 取消fork创建子进程操作
- 将线程的工作写在worker函数中,
- 创建让多个线程替代子进程传入worker函数去进行recv操作
void *worker(void *arg) {
int sockfd = *(int *)arg;
while (1) {
char buff[1024] = {0};
ssize_t rsize = recv(sockfd, buff, sizeof(buff), 0);//三次握手四次挥手
if (rsize > 0) {
printf("<receive> %s\n", buff);
} else {
close(sockfd);
break;
}
}
printf("<server> : a client has left!\n");
//exit(0);某个线程exit则其他线程都会被exit
}
//3.accept循环的接受客户端对server的连接
while (1) {
int newfd;//新建的文件描述符
struct sockaddr_in client;//用于存放临时建立连接的客户端的信息
socklen_t len = sizeof(client);
if ((newfd = accept(sockfd, (struct sockaddr *)&client, &len)) < 0) handle_error("accept");
printf("<accept> %s:%d: accept a client!\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//4.多线程recv接受消息
pthread_t tid;
pthread_create(&tid, NULL, worker, (void *)&newfd);
}
10个客户端全部成功连接上服务器,
但是当客户端发送消息时,并不是所有客户端都得到了响应!线程之间发生竞争,导致部分client发送的消息无法得到响应!
可以发现发送hello2、hello3消息的客户端没有得到任何响应,后续在这两个客户端中发送消息也得不到任何响应。
fix2
为解决线程对文件描述符的竞争问题(进程中多个线程共享空间),创建文件描述符数组、创建线程数组解决线程竞争问题
//3.accept循环的接受客户端对server的连接
pthread_t tid[MAXUSER + 5];//线程tid数组 用于解决线程竞争
int fd[MAXUSER + 5];//文件描述符数组 用于解决文件描述符竞争
while (1) {
int newfd;//新建的文件描述符
struct sockaddr_in client;//用于存放临时建立连接的客户端的信息
socklen_t len = sizeof(client);
if ((newfd = accept(sockfd, (struct sockaddr *)&client, &len)) < 0) handle_error("accept");
printf("<accept> %s:%d: accept a client!\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//4.多线程recv接受消息
fd[newfd] = newfd;//这样所使用的文件描述符 就是当前进程所打开的文件描述符中最小、未被使用的fd
pthread_create(&tid[newfd], NULL, worker, (void *)&fd[newfd]);//成功解决tid与fd竞争的问题
}
在创建结构体数组、创建线程数组之后,所有客户端发送的消息都得到了响应,线程竞争问题解决。
fix3
注意到,server中的端口号和ip没有进行显示,这是为了简化问题在worker函数中暂时将客户端ip与端口不显示(简化参数传递过程)。
创建线程时执行work函数涉及传参问题:需使用结构体将参数封装后,将结构体指针传入,才能获取到ip地址与端口号。
- 对worker函数进行修改,恢复ip地址与端口号的显示(对传入的结构体参数进行解封)
- 修改pthread_create线程创建中参数传递的过程,将传递的newfd参数改为传递结构体参数(同时包含newfd与client信息)
void *worker(void *arg) {
struct Data data = *(struct Data *)arg;//进行参数的类型转换
int sockfd = data.fd;
struct sockaddr_in client = data.client;//将参数进行解封装
while (1) {
char buff[1024] = {0};
ssize_t rsize = recv(sockfd, buff, sizeof(buff), 0);//三次握手四次挥手
if (rsize > 0) {
printf("<receive> %s:%d: %s\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), buff);
} else {
close(sockfd);
break;
}
}
printf("<server> : %s:%d has left!\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//exit(0);某个线程exit则其他线程都会被exit
}
struct Data {
int fd;
struct sockaddr_in client;
};
//3.accept循环的接受客户端对server的连接
pthread_t tid[MAXUSER + 5];//线程tid数组 用于解决线程竞争
struct Data data[MAXUSER + 5];//文件描述符与client的结构体数组 用于解决文件描述符竞争
while (1) {
int newfd;//新建的文件描述符
struct sockaddr_in client;//用于存放临时建立连接的客户端的信息
socklen_t len = sizeof(client);
if ((newfd = accept(sockfd, (struct sockaddr *)&client, &len)) < 0) handle_error("accept");
printf("<accept> %s:%d: accept a client!\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//4.多线程recv接受消息
data[newfd].client = client;
data[newfd].fd = newfd;//这样所使用的文件描述符 就是当前进程所打开的文件描述符中最小、未被使用的fd
//创建线程进行work工作涉及传参问题:需要使用结构体将参数封装后,将结构体指针传给线程
pthread_create(&tid[newfd], NULL, worker, (void *)&data[newfd]);//成功解决tid与fd竞争的问题
}
10个客户端都成功连接到服务器,并且所有客户端发送的消息都被server成功响应,并且每个客户端的ip地址与port端口号都正常显示。
socket多线程的修改完成。
sourcecode
以下是最终代码:
//server.c
#include "head.h"
#include "common.h"
#define MAXUSER 100
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
struct Data {
int fd;
struct sockaddr_in client;
};
void *worker(void *arg) {
struct Data data = *(struct Data *)arg;//进行参数的类型转换
int sockfd = data.fd;
struct sockaddr_in client = data.client;//将参数进行解封装
while (1) {
char buff[1024] = {0};
ssize_t rsize = recv(sockfd, buff, sizeof(buff), 0);//三次握手四次挥手
if (rsize > 0) {
printf("<receive> %s:%d: %s\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), buff);
} else {
close(sockfd);
break;
}
}
printf("<server> : %s:%d has left!\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//exit(0);某个线程exit则其他线程都会被exit
}
int main(int argc, char *argv[]) {
//./a.out -p port
//1.命令行解析
if (argc != 3) {
fprintf(stderr, "Usage : %s -p port", argv[0]);
exit(1);
}
int opt;
int port;
int sockfd;
while ((opt = getopt(argc, argv, "p:")) != -1) {
switch (opt) {
case 'p':
port = atoi(optarg);
break;
default:
fprintf(stderr, "Usage : %s -p port\n", argv[0]);
exit(1);
}
}
//2.创建socket
if ((sockfd = socketCreate(port)) < 0) handle_error("socketCreate");
//3.accept循环的接受客户端对server的连接
pthread_t tid[MAXUSER + 5];//线程tid数组 用于解决线程竞争
struct Data data[MAXUSER + 5];//文件描述符与client的结构体数组 用于解决文件描述符竞争
while (1) {
int newfd;//新建的文件描述符
struct sockaddr_in client;//用于存放临时建立连接的客户端的信息
socklen_t len = sizeof(client);
if ((newfd = accept(sockfd, (struct sockaddr *)&client, &len)) < 0) handle_error("accept");
printf("<accept> %s:%d: accept a client!\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//4.多线程recv接受消息
data[newfd].client = client;
data[newfd].fd = newfd;//这样所使用的文件描述符 就是当前进程所打开的文件描述符中最小、未被使用的fd
//创建线程进行work工作涉及传参问题:需要使用结构体将参数封装后,将结构体指针传给线程
pthread_create(&tid[newfd], NULL, worker, (void *)&data[newfd]);//成功解决tid与fd竞争的问题
}
return 0;
}
//client.c
#include "head.h"
#include "common.h"
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
int sockfd;
//ctrl+c信号处理
void closeSock(int signum) {
send(sockfd, "I am leaving...", 27, 0);
close(sockfd);//关闭客户端文件描述符
exit(0);
}
int main(int argc, char *argv[]) {
//./a.out ip port
if (argc != 3) {
fprintf(stderr, "Usage : %s ip port\n", argv[0]);
exit(1);
}
signal(SIGINT, closeSock);
//1.建立连接connect
if ((sockfd = socketConnect(argv[1], atoi(argv[2]))) < 0) handle_error("socketConnect");
printf("connect sccuess!\n");
//2.send发送数据
while (1) {//循环发送消息
char buff[1024] = {0};
scanf("%[^\n]s", buff);//输入可含空格的字符串
getchar();//吞掉一个换行
//只要向文件描述符中写入 tcp服务就会帮助发送消息
//With a zero flags argument, send is equivalent to write(2).
if (strlen(buff)) send(sockfd, buff, sizeof(buff), 0);
}
return 0;
}
//common.h
#ifndef _COMMON_H
#define _COMMON_H
int socketCreate(int port);
int socketConnect(const char *ip, int port);
#endif
//common.c
#include "head.h"
int socketCreate(int port) {
//1.创建套接字
int sockfd;
struct sockaddr_in addr;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) return -1;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);//主机字节序转换为网络字节序
addr.sin_addr.s_addr = inet_addr("0.0.0.0");//将网络字节序转化为主机字节序 0.0.0.0表示不关注消息传来的地址
//2.bind绑定套接字与结构体信息 listen
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0) return -1;
if (listen(sockfd, 20) < 0) return -1;
return sockfd;
}
int socketConnect(const char *ip, int port) {
int sockfd;
//1.客户端打开一个socket
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) return -1;
//2.定义结构体用于绑定端口号、ip地址(存放服务端的具体信息)
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(port);//端口号
server.sin_addr.s_addr = inet_addr(ip);//ip地址
//3.建立连接connection
if (connect(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0) return -1;
return sockfd;
}