欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

简单理解socket与多线程和实现Linux系统下多客户端的数据通信

程序员文章站 2022-03-01 20:43:39
...

学习总结

1、首先简单理解什么是socket

socket(套接字)是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

2、什么是多线程,有什么用?

首先我们要理解什么是线程与多线程。

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

多线程是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。

简单来说:线程是程序中一个单一的顺序控制流程;而多线程就是在单个进程中同时运行多个线程来完成不同的工作。

多线程的优缺点

优点:

1)、多线程技术可以加快程序的运行速度,使程序的响应速度更快,因为用户界面可以在进行其它工作的同时一直处于活动状态

2)、可以把占据长时间的程序中的任务放到后台去处理,同时执行其他操作,提高效率

3)、当前没有进行处理的任务时可以将处理器时间让给其它任务

4)、可以让同一个程序的不同部分并发执行,释放一些珍贵的资源如内存占用等等

5)、可以随时停止任务

6)、可以分别设置各个任务的优先级以优化性能

缺点:

1)、因为多线程需要开辟内存,而且线程切换需要时间因此会很消耗系统内存。

2)、线程的终止会对程序产生影响

3)、由于多个线程之间存在共享数据,因此容易出现线程死锁的情况

4)、对线程进行管理要求额外的 CPU开销。线程的使用会给系统带来上下文切换的额外负担。

3、代码实现

首先来完成一个服务端的创建

第一步需要先定义俩个全局变量


#define BUF_SIZE 100
//能够连接的最大数量
#define MAX_CLNT 256 

//客户端的连接数
int clnt_cnt = 0;
//客户端的连接sock的容器
int clnt_socks[MAX_CLNT];
//线程锁
pthread_mutex_t mutx;

这俩个全局变量的作用:
操作系统可以看作时一个工厂,进程是生产线,线程就是在这条生产线上的工人。
而这俩个变量就是用来记录工人数量和存储工人组的。

//第一个变量代表工人的数目
int clnt_cnt = 0;       //客户端链接个数
//第二个变量数组代表工人组
int clnt_socks[MAX_CLNT];//socket分机

第二步就是创建socket服务端

想要完成通信最少需要一个服务端和一个或者多个客户端

而服务端与客户端可以看成一个电话机的主机与分机

int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	int clnt_adr_sz;
	pthread_t t_id;
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
pthread_mutex_init(&mutx, NULL);

//1  安装一步电话机
serv_sock=socket(PF_INET, SOCK_STREAM, 0);

	memset(&serv_adr, 0, sizeof(serv_adr));
	//传输地址的地址族
	serv_adr.sin_family=AF_INET; 
	//设置指定IPv4传输地址
	//htonl 地址转化 INADDR_ANY 任何地址
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	//传输协议端口号
	//当服务端与客户段端口号不同是将无法连接成功
	serv_adr.sin_port=htons(atoi(argv[1]));
//2、分配地址和端口
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)//判断分配是否成功   失败返回错误号
		error_handling("bind() error");
//3、监听
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");//判断连接是否成功   失败返回错误号
		
		
    void error_handling(char * msg)
    {
	   fputs(msg, stderr);//输出错误号
	   fputc('\n', stderr);
	   exit(1);
    }

第三步 多线程链接处理

while(1)
{
    //每来一个链接,服务端要去处理客户端
   
    // 第一步 分配分机 处理此链接
    clnt_adr_sz = sizeof(clnt_adr);
    clnt_sock=accept(serv_sock, (struct sockaddr*) &clnt_adr, &clnt_adr_sz);
    // 第二步 让服务端维护的客户端数量加一
    pthread_mutex_lock(&mutx);//上锁
    clnt_socks[clnt_cnt++]=clnt_sock;//将工人的数据存储到工人组中
    pthread_mutex_unlock(&mutx);//解锁
    // 第三步 起一个线程去维护链接
    //函数 pthread_create  Linux系统下创建线程的函数
    pthread_create(
        &t_id,//新创建的线程ID指向的内存单元
        NULL, //线程属性,默认为NULL
        handle_clnt,//新创建的线程从 handle_clnt函数的地址开始运行
        (void*)&clnt_sock//运行函数的参数
    );
    //pthread_detach 实现线程分离
    pthread_detach(t_id);//让线程分离  --自动退出,无系统残留资源
    //打印当前链接的IP地址
    printf("ip = %s\n",inet_ntoa(clnt_adr.sin_addr));
    
}

第四步 收发消息的处理

接收到消息与某个客户端下线的处理

void * handle_clnt(void* arg)
{
	//第一步 把工人取过来了
	int clnt_sock = *((int *)arg);//获取消息
	int str_len = 0;//消息的长度
	int i;
	char msg[BUF_SIZE];//消息的容器
	//每次收到来自客户端的消息
	//read   读取数据
	while((str_len = read(clnt_sock, msg, sizeof(msg)))!=0)//判断是否收到消息
	{
		//将收到的消息发给所有的客户端
		send_msg(msg, str_len);
	}
     
     pthread_mutex_lock(&mutx);//上锁
    //当某个客户端下线了
    //首先 通过遍历整个工作组找到下线的客户端
	for(i=0; i<clnt_cnt; i++) //for是查找所有的socket有没有clnt_sock
	{
		if(clnt_sock == clnt_socks[i]) //删除clnt_sock,并且迁移
		{
			while(i++ < clnt_cnt-1)
			{
				clnt_socks[i] = clnt_socks[i+1];
			}
			break;
		}
	}
	pthread_mutex_unlock(&mutx);//解锁
	close(clnt_sock);
	
	return NULL;
}

给所有客户端发消息

void send_msg(char * msg, int len)   // send to all
{
	int i;
	pthread_mutex_lock(&mutx);//上锁
	//通过循环的方式向每个在线的客户端发送消息
	for(i=0; i<clnt_cnt; i++)
	{
		//write 写数据(发送数据)   
		write(clnt_socks[i], msg, len);
	}
	
	pthread_mutex_unlock(&mutx);//解锁
}

第五步 线程锁

线程锁:主要用来给方法、代码块加锁。当某个方法或者代码块使用锁时,那么在同一时刻至多仅有有一个线程在执行该段代码。当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。但是,其余线程是可以访问该对象中的非加锁代码块的。

//线程锁
pthread_mutex_t mutx;

//加上线程锁以后 可以保证每次只有一个工人存储到工人组中,这样就保证了所有线程能够按照我们所设定的顺序执行
pthread_mutex_lock(&mutx);//上锁
clnt_socks[clnt_cnt++]=clnt_sock;//将工人的数据存储到工人组中
pthread_mutex_unlock(&mutx);//解锁

以上代码我们就完成了一个服务端的创建。

创建服务端就需要创建客户端来与服务端通信。

第一步 先定义俩个全局变量数组

#define BUF_SIZE 100
#define NAME_SIZE 20
//客户端的名称
char name[NAME_SIZE]="[DEFAULT]";
//消息的容器
char msg[BUF_SIZE];

第二部 与服务端相同,创建一个电话机

    int sock;
	struct sockaddr_in serv_addr;
	pthread_t snd_thread, rcv_thread;
	void * thread_return;
	if(argc!=4) {
		printf("Usage : %s <IP> <port> <name>\n", argv[0]);
		exit(1);
	 }
	
	//输入 当前客户端的名称
	sprintf(name, "[%s]", argv[3]);
	// 创建 socket 
	sock=socket(PF_INET, SOCK_STREAM, 0);
	// 配置要连接的服务器
	memset(&serv_addr, 0, sizeof(serv_addr));//按字节对内存块进行初始化
	//传输地址的地址族
	serv_addr.sin_family=AF_INET;
	//设置指定IPv4传输地址
	serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
	//传输协议端口号
	serv_addr.sin_port=htons(atoi(argv[2]));
	 
	 //连接服务端
	if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
		error_handling("connect() error");//判断是否连接成功   连接失败返回错误号
		
		void error_handling(char *msg)
       {
       //输出错误号
	   fputs(msg, stderr);
	   //每次输出完后换行
	   fputc('\n', stderr);
	   exit(1);
       }

第三步 接收消息

//创建一个线程  并处理从服务端接收到的消息
pthread_create(&snd_thread, NULL, send_msg, (void*)&sock);

void * send_msg(void * arg)   // send thread main
{
    //接收到的消息
	int sock=*((int*)arg);
	//存放 客户端名称与消息内容 
	char name_msg[NAME_SIZE+BUF_SIZE];
	while(1) 
	{
		//接收来自于控制台的消息
        //读取数据
		fgets(msg, BUF_SIZE, stdin);
		//收到的消息为 q或者Q 时关闭线程与客户端
		if(!strcmp(msg,"q\n")||!strcmp(msg,"Q\n")) 
		{
			close(sock);//关闭线程
			exit(0);    //退出客户端
		}
		//打印 发送消息的客户端名称与收到的消息
		sprintf(name_msg,"%s %s", name, msg);
		//将数据写入到sock中
		write(sock, name_msg, strlen(name_msg));
	}
	return NULL;
}

第四步 发送消息

//创建一个线程  用于处理向服务器发送消息
     pthread_create(&rcv_thread, NULL, recv_msg,      (void*)&sock);

void * recv_msg(void * arg)   // read thread main
{
    
    //接收到的消息
	int sock=*((int*)arg);
	//存放 客户端名称与消息内容
	char name_msg[NAME_SIZE+BUF_SIZE];
	//消息内容的长度
	int str_len;
	while(1)
	{
	    //读取消息内容
		str_len=read(sock, name_msg, NAME_SIZE+BUF_SIZE-1);
		//判断消息内容是否为空
		if(str_len==-1) 
			return (void*)-1;
		//初始化容器
		name_msg[str_len]=0;
		//写入需要发送的消息内容
		fputs(name_msg, stdout);
	}
	return NULL;
}

第五步 线程同步

//函数pthread_join用来等待一个线程的结束,线程间同步的操作。  
//以阻塞的方式等待thread指定的线程结束 
pthread_join(
snd_thread, //thread:线程标识符,即线程ID   
&thread_return//retvat:用户定义的指针,用来存储被等待线程的返回值
);
pthread_join(rcv_thread, &thread_return);

第六步 关闭分机

close(sock);

总结,多客户端的通信是通过客户端发送数据包到服务端,然后通过服务端中转,发送到另一个客户端。

以上代码会用到的头文件:

服务端头文件:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

客户端头文件:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>