搞了一下午的Linux套接字,實現了多客戶端之間的TCP通信。不想再碼字了,就簡單描述一下代碼流程,直接把代碼貼出來吧。
服務端多線程的思路主要參考了這篇:Linux C利用Socket套接字進行服務器與多個客戶端進行通訊
感覺自己對線程和TCP的理解也不是特別清晰,以下內容有不會的地方請大家指正
程序說明
- 實現了多客戶端之間的TCP通信
- 可以更改最大客戶端數量
- 使用多線程處理客戶端鏈路
- 使用父子進程實現客戶端數據收發
程序流程
客戶機
- 客戶端只需考慮如何連接目標服務器即可
- 客戶端首先創建套接字並向服務器附送網絡連接請求
- 連接到服務器之後,創建子進程
- 父進程用來向服務器發送數據,子進程從服務器接收數據
服務器
- 服務器首先創建套接字並綁定網絡信息
- 之後創建一個子線程用於接收客戶端的網絡請求
- 在子線程中接收客戶端的網絡請求,併爲每一個客戶端創建新的子線程,該子線程用於服務器接收客戶端數據
- 服務器的主線程用於向所有客戶端循環發送數據
- 服務端大概流程如下圖所示
程序代碼
服務端程序
/********************************************************************
* 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 多客戶端通信測試
視頻裏面左側的終端是在Windows電腦開的虛擬機終端;中間的終端是同一個Windows電腦用FinalShell連接的服務器終端;右側的終端是另一臺Linux系統小電腦的終端。
致謝
特別感謝王文州同學,我們共同完成了該程序。