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系統小電腦的終端。

致謝

特別感謝王文州同學,我們共同完成了該程序。

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