【Linux系统与网络编程】13:Socket多线程

Socket多线程


在这里插入图片描述

fix1

将Socket多进程实现的聊天程序修改为多线程实现:

  1. 取消fork创建子进程操作
  2. 将线程的工作写在worker函数中,
  3. 创建让多个线程替代子进程传入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地址与端口号。

  1. 对worker函数进行修改,恢复ip地址与端口号的显示(对传入的结构体参数进行解封)
  2. 修改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;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值