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...\n");

    /* 创建TCP连接的Socket套接字 */
    int socketListen = socket(AF_INET, SOCK_STREAM, 0);
    if(socketListen < 0){
        printf("Fail to create Socket\n");
        exit(-1);
    }else{
        printf("Create Socket successful.\n");
    }

    /* 填充服务器端口地址信息,以便下面使用此地址和端口监听 */
    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\n");

    /* 开始监听相应的端口 */
    if(listen(socketListen, 10) != 0){
        perror("call listen()");
        exit(-1);
    }
    printf("call listen() successful\n");

    /* 创建一个线程用来接收客户端的连接请求 */
    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\n");
                    thrReceiveClientCount --;
                }
            }
            printf("Number of connected client: %d\n", thrReceiveClientCount);
            if(conClientCount <= 0){
                printf("No Clients!\n");
            }

            // 向所有客户端发送消息
            else{
                printf("conClientCount = %d\n", conClientCount);
                for(i = 0; i < conClientCount; i++){
                    printf("socketCon = %d\nbuffer is: %s\n", 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\n", arrConSocket[i].ipaddr, arrConSocket[i].port);
                        ReadToSend = 0;
                    }else{
                        printf("Fail to send message to %s:%d\n", arrConSocket[i].ipaddr, arrConSocket[i].port);
                    }
                }
            }
        }
        sleep(0.5);
    }

    /* 等待子进程退出 */
    printf("Waiting for child thread to exit ....\n");
    char *message;
    pthread_join(thrAccept,(void *)&message);
    printf("%s\n",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\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);
        }
        printf("Client socket: %d\n", 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\n", 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.\n");
 
        /* 让进程休息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 ...\n", _socketInfo.socketCon);
        buffer_length = recv(_socketInfo.socketCon, buffer, BUFSIZ, 0);
        if(buffer_length == 0){
            // 判断为客户端退出
            printf("%s:%d Closed!\n", _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()\n");
            break;
        }
        buffer[buffer_length] = '\0';
        printf("%s:%d said:%s\n", _socketInfo.ipaddr, _socketInfo.port, buffer);
        ReadToSend = 1;     // 发送标志置位,允许主线程发送数据
        usleep(100000);
    }
    printf("%s:%d Exit\n", _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\n");

	/* 从服务器接收初始化的握手消息 */
	len = recv(client_sockfd, buf_recv, BUFSIZ, 0);			//接收服务器端信息
    buf_recv[len] = '\0';
	printf("%s", buf_recv);									//打印服务器端的欢迎信息
	printf("Enter string to send: \n");
	
	/* 创建父子进程与服务器进行通信 */
	if((pid = fork()) < 0){
		printf("Fail to call fork()\n");
		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\n", 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!\n"
#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系统小电脑的终端。

致谢

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章