搞了一下午的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...
");
/* 创建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 多客户端通信测试
视频里面左侧的终端是在Windows电脑开的虚拟机终端;中间的终端是同一个Windows电脑用FinalShell连接的服务器终端;右侧的终端是另一台Linux系统小电脑的终端。
致谢
特别感谢王文州同学,我们共同完成了该程序。