zoukankan      html  css  js  c++  java
  • Linux Socket 多客户端通信

    搞了一下午的Linux套接字,实现了多客户端之间的TCP通信。不想再码字了,就简单描述一下代码流程,直接把代码贴出来吧。
    服务端多线程的思路主要参考了这篇:Linux C利用Socket套接字进行服务器与多个客户端进行通讯
    感觉自己对线程和TCP的理解也不是特别清晰,以下内容有不会的地方请大家指正

    程序说明

    1. 实现了多客户端之间的TCP通信
    2. 可以更改最大客户端数量
    3. 使用多线程处理客户端链路
    4. 使用父子进程实现客户端数据收发

    程序流程

    客户机

    • 客户端只需考虑如何连接目标服务器即可
    • 客户端首先创建套接字并向服务器附送网络连接请求
    • 连接到服务器之后,创建子进程
    • 父进程用来向服务器发送数据,子进程从服务器接收数据

    服务器

    • 服务器首先创建套接字并绑定网络信息
    • 之后创建一个子线程用于接收客户端的网络请求
    • 在子线程中接收客户端的网络请求,并为每一个客户端创建新的子线程,该子线程用于服务器接收客户端数据
    • 服务器的主线程用于向所有客户端循环发送数据
    • 服务端大概流程如下图所示
      服务器流程

    程序代码

    服务端程序

    /********************************************************************
    *   File Name: server.c
    *   Description: 用于实现多客户端通信
    *   Others: 
    *     1. 服务器首先创建套接字并绑定网络信息
    *     2. 之后创建一个子线程用于接收客户端的网络请求
    *     3. 在子线程中接收客户端的网络请求,并为每一个客户端创建新的子线程,该子线程用于服务器接收客户端数据
    *     4. 服务器的主线程用于向所有客户端循环发送数据
    *   Init Date: 2020/05/24
    *********************************************************************/
    #include "mysocket.h"
    
    int main()
    {
        ReadToSend = 0;
        conClientCount = 0;
        thrReceiveClientCount = 0;
    
        printf("Start Server...
    ");
    
        /* 创建TCP连接的Socket套接字 */
        int socketListen = socket(AF_INET, SOCK_STREAM, 0);
        if(socketListen < 0){
            printf("Fail to create Socket
    ");
            exit(-1);
        }else{
            printf("Create Socket successful.
    ");
        }
    
        /* 填充服务器端口地址信息,以便下面使用此地址和端口监听 */
        struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        // 这里的地址使用所有本地网络设备的地址,表示服务器会接收任意地址的客户端信息
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        server_addr.sin_port = htons(SERVER_PORT);
    
        /* 将套接字绑定到服务器的网络地址上 */
        if(bind(socketListen, (struct sockaddr *)&server_addr,sizeof(struct sockaddr)) != 0){
            perror("bind error");
    		exit(-1);
        }
        printf("call bind() successful
    ");
    
        /* 开始监听相应的端口 */
        if(listen(socketListen, 10) != 0){
            perror("call listen()");
            exit(-1);
        }
        printf("call listen() successful
    ");
    
        /* 创建一个线程用来接收客户端的连接请求 */
        pthread_t thrAccept;
        pthread_create(&thrAccept, NULL, fun_thrAcceptHandler, &socketListen);
    
        /* 主线程用来向所有客户端循环发送数据 */
        while(1){
            if(ReadToSend){
                // 判断线程存活数量
                int i;
                for(i = 0; i < thrReceiveClientCount; i++){
                    if(checkThrIsKill(arrThrReceiveClient[i])){
                        printf("A Thread has been killed
    ");
                        thrReceiveClientCount --;
                    }
                }
                printf("Number of connected client: %d
    ", thrReceiveClientCount);
                if(conClientCount <= 0){
                    printf("No Clients!
    ");
                }
    
                // 向所有客户端发送消息
                else{
                    printf("conClientCount = %d
    ", conClientCount);
                    for(i = 0; i < conClientCount; i++){
                        printf("socketCon = %d
    buffer is: %s
    ", arrConSocket[i].socketCon, buffer);
                        int sendMsg_len = send(arrConSocket[i].socketCon, buffer, strlen(buffer), 0);
                        if(sendMsg_len > 0){
                            printf("Send Message to %s:%d successful
    ", arrConSocket[i].ipaddr, arrConSocket[i].port);
                            ReadToSend = 0;
                        }else{
                            printf("Fail to send message to %s:%d
    ", arrConSocket[i].ipaddr, arrConSocket[i].port);
                        }
                    }
                }
            }
            sleep(0.5);
        }
    
        /* 等待子进程退出 */
        printf("Waiting for child thread to exit ....
    ");
        char *message;
        pthread_join(thrAccept,(void *)&message);
        printf("%s
    ",message);
    
        return 0;
    }
    
    
    
    /********************************************************************
    *   Function Name: void *fun_thrAcceptHandler(void *socketListen)
    *   Description: 监听客户端的连接请求,获取待连接客户端的网络信息,并为该客户端创建子线程.
    *   Called By: server.c[main]
    *   Input: socketListen -> 表示用于监听的被动套接字
    *   Date: 2020/05/24
    *********************************************************************/
    void *fun_thrAcceptHandler(void *socketListen){
        while(1){
            int sockaddr_in_size = sizeof(struct sockaddr_in);
            struct sockaddr_in client_addr;
            int _socketListen = *((int *)socketListen);
    
            /* 接收相应的客户端的连接请求 */
            int socketCon = accept(_socketListen, (struct sockaddr *)(&client_addr), (socklen_t *)(&sockaddr_in_size));
            if(socketCon < 0){
                printf("call accept()");
            }else{
                printf("Connected %s:%d
    ", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);
            }
            printf("Client socket: %d
    ", socketCon);
    
            /* 获取新客户端的网络信息 */
            _MySocketInfo socketInfo;
            socketInfo.socketCon = socketCon;
            socketInfo.ipaddr = inet_ntoa(client_addr.sin_addr);
            socketInfo.port = client_addr.sin_port;
    
            /* 将新客户端的网络信息保存在 arrConSocket 数组中 */
            arrConSocket[conClientCount] = socketInfo;
            conClientCount++;
            printf("Number of users: %d
    ", conClientCount);
    
            /* 为新连接的客户端开辟线程 fun_thrReceiveHandler,该线程用来循环接收客户端的数据 */
            pthread_t thrReceive = 0;
            pthread_create(&thrReceive, NULL, fun_thrReceiveHandler, &socketInfo);
            arrThrReceiveClient[thrReceiveClientCount] = thrReceive;
            thrReceiveClientCount ++;
            printf("A thread has been created for the user.
    ");
     
            /* 让进程休息0.1秒 */
            usleep(100000);
        }
     
        char *s = "Safe exit from the receive process ...";
        pthread_exit(s);
    }
    
    
    
    /********************************************************************
    *   Function Name: void *fun_thrReceiveHandler(void *socketInfo)
    *   Description: 向服务器发送初始消息,从服务器循环接收信息.
    *   Called By: server.c[main]
    *   Input: socketInfo -> 表示客户端的网络信息
    *   Date: 2020/05/24
    *********************************************************************/
    void *fun_thrReceiveHandler(void *socketInfo){
    	int buffer_length;
        int con;
        int i;
    	_MySocketInfo _socketInfo = *((_MySocketInfo *)socketInfo);
    
        /* 向服务器发送握手消息 */
        send(_socketInfo.socketCon, HANDSHARK_MSG, sizeof(HANDSHARK_MSG), 0);
    
        /* 从服务器循环接收消息 */
        while(1){
    
        	// 将接收缓冲区buffer清空
        	bzero(&buffer,sizeof(buffer));
    
            // 接收服务器信息
            printf("Receiving messages from client %d ...
    ", _socketInfo.socketCon);
            buffer_length = recv(_socketInfo.socketCon, buffer, BUFSIZ, 0);
            if(buffer_length == 0){
                // 判断为客户端退出
                printf("%s:%d Closed!
    ", _socketInfo.ipaddr, _socketInfo.port);
                // 找到该客户端在数组中的位置
                for(con = 0; con < conClientCount; con++){
                    if(arrConSocket[con].socketCon == _socketInfo.socketCon){
                        break;
                    }
                }
                // 将该客户端的信息删除,重置客户端数组
                for(i = con; i < conClientCount-1; i++){
                    arrConSocket[i] = arrConSocket[i+1];
                }
                conClientCount --;
                break;
            }
            else if(buffer_length < 0){
                printf("Fail to call read()
    ");
                break;
            }
            buffer[buffer_length] = '';
            printf("%s:%d said:%s
    ", _socketInfo.ipaddr, _socketInfo.port, buffer);
            ReadToSend = 1;     // 发送标志置位,允许主线程发送数据
            usleep(100000);
        }
        printf("%s:%d Exit
    ", _socketInfo.ipaddr, _socketInfo.port);
        return NULL;
    }
    
    
    
    /********************************************************************
    *   Function Name: checkThrIsKill(pthread_t thr)
    *   Description: 检测当前线程是否存活.
    *   Called By: server.c[main]
    *   Input: thr -> 线程数组中的线程
    *   Date: 2020/05/24
    *********************************************************************/
    int checkThrIsKill(pthread_t thr){
        int res = 1;
        int res_kill = pthread_kill(thr, 0);
        if(res_kill == 0){
            res = 0;
        }
        return res;
    }
    

    客户端程序

    /********************************************************************
    *   File Name: client.c
    *   Description: 用于实现多客户端通信
    *   Others: 
    *     1. 客户端只需考虑如何连接目标服务器即可
    *     2. 客户端首先创建套接字并向服务器附送网络连接请求
    *     3. 连接到服务器之后,创建子进程
    *     4. 父进程用来向服务器发送数据,子进程从服务器接收数据
    *   Init Date: 2020/05/24
    *********************************************************************/
    #include "mysocket.h"
    
    int main(int argc, char *argv[]){
    	int client_sockfd;
    	int len;
    	pid_t pid;
        char buf_recv[BUFSIZ];                                 	//数据传送的缓冲区
    	char buf_send[BUFSIZ];
    	struct sockaddr_in remote_addr;                         //服务器端网络地址结构体
    
    	/* 初始化目标服务器的网络信息 */
    	memset(&remote_addr, 0, sizeof(remote_addr));           //数据初始化--清零
    	remote_addr.sin_family = AF_INET;                       //设置为IP通信
    	remote_addr.sin_addr.s_addr = inet_addr(SERVER_IP);     //服务器IP地址
    	remote_addr.sin_port = htons(SERVER_PORT);              //服务器端口号
    	
    	/*创建客户端套接字--IPv4协议,面向连接通信,TCP协议*/
    	if((client_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
    		perror("socket error");
    		return 1;
    	}
    	
    	/*将套接字绑定到服务器的网络地址上*/
    	if(connect(client_sockfd, (struct sockaddr *)&remote_addr, sizeof(struct sockaddr)) < 0){
    		perror("connect error");
    		return 1;
    	}
    	printf("connected to server
    ");
    
    	/* 从服务器接收初始化的握手消息 */
    	len = recv(client_sockfd, buf_recv, BUFSIZ, 0);			//接收服务器端信息
        buf_recv[len] = '';
    	printf("%s", buf_recv);									//打印服务器端的欢迎信息
    	printf("Enter string to send: 
    ");
    	
    	/* 创建父子进程与服务器进行通信 */
    	if((pid = fork()) < 0){
    		printf("Fail to call fork()
    ");
    		return 1;
    	}
    	
    	/* 父进程用来发送数据 */
    	else if(pid > 0){
    		while(1){
    			scanf("%s", buf_send);
    			if(!strcmp(buf_send, "quit")){
    				kill(pid, SIGSTOP);
    				break;
    			}
    			len = send(client_sockfd, buf_send, strlen(buf_send), 0);
    		}
    	}
    	/* 子进程用来接收数据 */
    	else{
    		while(1){
    			memset(buf_recv, 0, sizeof(buf_recv));
    			if((len = recv(client_sockfd, buf_recv, BUFSIZ, 0)) > 0){
    				printf("Recive from server: %s
    ", buf_recv);
    			}
    			usleep(200000);
    		}
    	}
    
    	/* 关闭套接字 */
    	close(client_sockfd);
    
    	return 0;
    }
    
    

    头文件

    /********************************************************************
    *   File Name: mysocket.h
    *   Description: 用于实现多客户端通信
    *   Init Date: 2020/05/24
    *********************************************************************/
    #include <stdlib.h>
    #include <sys/types.h>
    #include <stdio.h>
    #include <sys/socket.h>
    #include <string.h>
    #include <signal.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <pthread.h>
    
    #define SERVER_IP "127.0.0.1"       // 用于本地测试
    // #define SERVER_IP "47.95.13.239"	// 用于公网测试
    #define SERVER_PORT 18888
    #define HANDSHARK_MSG "Hello,Client!
    "
    #define MaxClientNum 10
    
    /* 套接字信息结构体,用于记录客户端信息 */
    typedef struct MySocketInfo{
        int socketCon;                  // 套接字描述符
        char *ipaddr;                   // 客户端IP地址
        uint16_t port;                  // 客户端端口号
    }_MySocketInfo;
    
    char buffer[BUFSIZ];                // 服务器数据收发缓冲区
    int ReadToSend;                     // 服务器准备发送标志位
    
    /* 用于记录客户端信息的数组 */
    struct MySocketInfo arrConSocket[MaxClientNum];
    int conClientCount;                 // 当前客户端数量
    
    /* 用来与客户端通信的线程数组 */
    pthread_t arrThrReceiveClient[MaxClientNum];
    int thrReceiveClientCount;          // 当前通信子线程数量
    
    /* 线程功能函数 */
    void *fun_thrReceiveHandler(void *socketInfo);
    void *fun_thrAcceptHandler(void *socketListen);
    int checkThrIsKill(pthread_t thr);
    
    

    Makefile

    all:
    	gcc server.c -o server -lpthread
    	gcc client.c -o client
    

    程序测试

    测试说明

    将服务端程序放到服务器上,即可实现多客户端之间的公网通信。
    修改头文件里的SERVER_IP宏定义,可以改变测试环境。
    修改头文件里的MaxClientNumber宏定义,可以改变最大客户端数量。
    我测试时将服务端程序放到了阿里云的服务器上,然后电脑连上手机热点,另一台小电脑连上家里的WiFi,运行程序测试通信功能。也就是说两台电脑不在同一个局域网中。

    测试视频

    测试视频如下。

    Linux Socket 多客户端通信测试

    点击这里可前往B站查看更清晰的测试视频

    视频里面左侧的终端是在Windows电脑开的虚拟机终端;中间的终端是同一个Windows电脑用FinalShell连接的服务器终端;右侧的终端是另一台Linux系统小电脑的终端。

    致谢

    特别感谢王文州同学,我们共同完成了该程序。

  • 相关阅读:
    spring-mvc dispatcherServlet
    常用注解
    spring基础
    消息转换
    高级装配
    Leetcode第242题:有效的字母异位词
    Leetcode第76题:最小覆盖子串
    Leetcode633题平方数之和
    Leetcode454题四数之和II
    java从虚拟机执行角度解析案例(转)
  • 原文地址:https://www.cnblogs.com/ZHJ0125/p/12968838.html
Copyright © 2011-2022 走看看