马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
一、体系概述
本体系是一个基于 TCP 协议的多人谈天体系,由一个服务器和多个客户端构成。客户端可以毗连到服务器,向服务器发送消息,服务器吸取到消息后将其转发给其他客户端,实现多人之间的及时谈天。体系使用 C 语言编写,使用了 Unix 体系的网络编程接口和多线程、I/O 多路复用等技能。
二、文件布局
- server.c:服务器端步调,负责监听客户端毗连、吸取客户端消息并将消息转发给其他客户端。
- client1.c:客户端步调,使用 poll 函数实现 I/O 多路复用,同时处置处罚用户输入和服务器消息。
- client2.c:客户端步调,使用多线程技能,一个线程负责吸取服务器消息,另一个线程负责处置处罚用户输入。
三、代码具体分析
1. 数据包布局体
在三个文件中都界说了雷同的数据包布局体 Packet,用于在客户端和服务器之间传输数据。- typedef struct {
- int type; // 0 for message, 1 for disconnect
- char data[BUFFER_SIZE];
- } Packet;
复制代码
- type:数据包范例,0 表现消息,1 表现断开毗连。
- data:数据包携带的数据,最大长度为 BUFFER_SIZE。
2. 服务器端步调(server.c)
2.1 重要变量
- server_fd:服务器套接字文件形貌符。
- client_fd:客户端套接字文件形貌符。
- max_fd:记载最大的文件形貌符,用于 select 函数。
- activity:记载 select 函数返回的运动文件形貌符数目。
- valread:记载从客户端读取的数据长度。
- server_addr:存储服务器的所在信息。
- client_addr:存储客户端的所在信息。
- client_sockets:数组用于存储全部客户端的套接字文件形貌符。
- readfds:文件形貌符聚集,用于 select 函数监听可读变乱。
2.2 重要步调
- 创建套接字:使用 socket 函数创建一个 TCP 套接字。
- 绑定所在:使用 bind 函数将套接字绑定到指定的所在和端口。
- 监听毗连:使用 listen 函数开始监听客户端毗连。
- 循环处置处罚:使用 select 函数监听服务器套接字和客户端套接字的可读变乱。
- 若服务器套接字有可读变乱,阐明有新的客户端毗连哀求,使用 accept 函数担当毗连。
- 若客户端套接字有可读变乱,从客户端读取数据包,根据数据包范例举行相应处置处罚。
- 若数据包范例为消息,将消息转发给其他客户端。
- 若数据包范例为断开毗连或读取到的数据长度为 0,阐明客户端断开毗连,关闭客户端套接字。
3. 客户端步调(client1.c)
3.1 重要变量
- client_fd:客户端套接字文件形貌符。
- server_addr:存储服务器的所在信息。
- packet:用于存储要发送或吸取的数据包。
- fds:数组用于存储要监听的文件形貌符及其变乱。
3.2 重要步调
- 创建套接字:使用 socket 函数创建一个 TCP 套接字。
- 毗连服务器:使用 connect 函数毗连到服务器。
- 初始化 poll 布局体:监听标准输入和客户端套接字的可读变乱。
- 循环处置处罚:使用 poll 函数监听文件形貌符集会合的可读变乱。
- 若标准输入有可读变乱,从标准输入读取数据,设置数据包范例为消息,发送给服务器。
- 若客户端套接字有可读变乱,从服务器读取数据包,根据数据包范例举行相应处置处罚。
- 若数据包范例为消息,输出吸取到的消息。
- 若数据包范例为断开毗连或读取数据失败,阐明服务器断开毗连,关闭客户端套接字,跳出循环。
4. 客户端步调(client2.c)
4.1 重要变量
- client_fd:客户端套接字文件形貌符。
- server_addr:存储服务器的所在信息。
- packet:用于存储要发送或吸取的数据包。
- thread_id:存储线程的标识符。
4.2 重要步调
- 创建套接字:使用 socket 函数创建一个 TCP 套接字。
- 毗连服务器:使用 connect 函数毗连到服务器。
- 创建线程:创建一个线程来吸取服务器发送的消息。
- 循环处置处罚:在主线程中,从标准输入读取数据,设置数据包范例为消息,发送给服务器。
- 线程函数:在子线程中,一连吸取服务器消息,根据数据包范例举行相应处置处罚。
- 若数据包范例为消息,输出吸取到的消息。
- 若数据包范例为断开毗连或读取数据失败,阐明服务器断开毗连,关闭客户端套接字,退出步调。
四、编译和运行
4.1 编译
- gcc server.c -o server
- gcc client1.c -o client1
- gcc client2.c -o client2 -lpthread
复制代码 4.2 运行
4.3运行效果展示
五、源码
5.1服务器端步调(server.c)
- #include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>#include <sys/select.h>#include <sys/time.h>#include <sys/types.h>#include <fcntl.h>#include <errno.h>// 界说服务器监听的端标语#define PORT 8080// 界说数据缓冲区的巨细#define BUFFER_SIZE 1024// 界说服务器允许的最大客户端毗连数#define MAX_CLIENTS 10/** * 界说数据包布局体,用于在服务器和客户端之间传输数据 * type 数据包范例,0 表现消息,1 表现断开毗连 * data 数据包携带的数据 */
- typedef struct {
- int type; // 0 for message, 1 for disconnect
- char data[BUFFER_SIZE];
- } Packet;/** * 主函数,服务器步调的入口点 * @return 步调的退出状态码,0 表现正常退出 */int main() { // server_fd 为服务器套接字文件形貌符,client_fd 为客户端套接字文件形貌符 // max_fd 记载最大的文件形貌符,用于 select 函数 // activity 记载 select 函数返回的运动文件形貌符数目 // valread 记载从客户端读取的数据长度 int server_fd, client_fd, max_fd, activity, valread; // server_addr 存储服务器的所在信息,client_addr 存储客户端的所在信息 struct sockaddr_in server_addr, client_addr; // client_len 存储客户端所在布局体的长度 socklen_t client_len = sizeof(client_addr); // packet 用于存储从客户端吸取的数据包 Packet packet; // client_sockets 数组用于存储全部客户端的套接字文件形貌符 int client_sockets[MAX_CLIENTS] = {0}; // readfds 是一个文件形貌符聚集,用于 select 函数监听可读变乱 fd_set readfds; // 创建服务器套接字,使用 IPv4 所在族和 TCP 协议 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { // 若套接字创建失败,输堕落误信息并退出步调 perror("socket failed"); exit(EXIT_FAILURE); } // 设置服务器所在信息 server_addr.sin_family = AF_INET; // 监听全部可用的网络接口 server_addr.sin_addr.s_addr = INADDR_ANY; // 将端标语从主机字节序转换为网络字节序 server_addr.sin_port = htons(PORT); // 将服务器套接字绑定到指定的所在和端口 if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { // 若绑定失败,输堕落误信息,关闭套接字并退出步调 perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } // 开始监听客户端毗连,允许的最大毗连哀求队列长度为 3 if (listen(server_fd, 3) < 0) { // 若监听失败,输堕落误信息,关闭套接字并退出步调 perror("listen"); close(server_fd); exit(EXIT_FAILURE); } // 输出服务器启动信息,表现监听的端标语 printf("Server started on port %d\n", PORT); // 进入无穷循环,一连处置处罚客户端毗连和数据 while (1) { // 清空文件形貌符聚集 FD_ZERO(&readfds); // 将服务器套接字添加到文件形貌符集会合,监听其可读变乱 FD_SET(server_fd, &readfds); // 初始化最大文件形貌符为服务器套接字文件形貌符 max_fd = server_fd; // 遍历客户端套接字数组 for (int i = 0; i < MAX_CLIENTS; i++) { // 获取当前客户端的套接字文件形貌符 int sd = client_sockets[i]; if (sd > 0) { // 若该客户端套接字有效,将其添加到文件形貌符集会合 FD_SET(sd, &readfds); } if (sd > max_fd) { // 更新最大文件形貌符 max_fd = sd; } } // 调用 select 函数监听文件形貌符集会合的可读变乱,无超时时间 activity = select(max_fd + 1, &readfds, NULL, NULL, NULL); if ((activity < 0) && (errno != EINTR)) { // 若 select 函数调用失败且不是被信号停止,输堕落误信息 perror("select error"); } if (FD_ISSET(server_fd, &readfds)) { // 若服务器套接字有可读变乱,阐明有新的客户端毗连哀求 if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len)) < 0) { // 若担当毗连失败,输堕落误信息并继续循环 perror("accept"); continue; } // 输出新客户端毗连的信息,包罗套接字文件形貌符、IP 所在和端标语 printf("New connection, socket fd is %d, ip is : %s, port : %d\n", client_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 遍历客户端套接字数组,找到一个空闲位置存储新客户端的套接字文件形貌符 for (int i = 0; i < MAX_CLIENTS; i++) { if (client_sockets[i] == 0) { client_sockets[i] = client_fd; break; } } } // 遍历客户端套接字数组,查抄每个客户端是否有可读变乱 for (int i = 0; i < MAX_CLIENTS; i++) { int sd = client_sockets[i]; if (FD_ISSET(sd, &readfds)) { // 从客户端读取数据包 valread = read(sd, &packet, sizeof(Packet)); if (valread == 0) { // 若读取到的数据长度为 0,阐明客户端断开毗连 getpeername(sd, (struct sockaddr*)&client_addr, &client_len); // 输出客户端断开毗连的信息,包罗 IP 所在和端标语 printf("Host disconnected, ip %s, port %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 关闭客户端套接字 close(sd); // 将该位置的客户端套接字文件形貌符置为 0,表现空闲 client_sockets[i] = 0; } else { if (packet.type == 0) { // 若数据包范例为消息,输出吸取到的消息 printf("Received from client %d: %s", sd, packet.data); // 将消息转发给其他客户端 for (int j = 0; j < MAX_CLIENTS; j++) { if (client_sockets[j] != sd && client_sockets[j] != 0) { // 发送数据包给其他客户端 send(client_sockets[j], &packet, sizeof(Packet), 0); } } } else if (packet.type == 1) { // 若数据包范例为断开毗连,输出客户端断开毗连的信息 printf("Client %d disconnected\n", sd); // 关闭客户端套接字 close(sd); // 将该位置的客户端套接字文件形貌符置为 0,表现空闲 client_sockets[i] = 0; } } } } } return 0;}
复制代码 5.2客户端步调(client1.c)
- #include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <arpa/inet.h>#include <poll.h>#include <errno.h>// 界说服务器监听的端标语#define PORT 8080// 界说数据缓冲区的巨细#define BUFFER_SIZE 1024/** * 界说数据包布局体,用于在客户端和服务器之间传输数据 * type 数据包范例,0 表现消息,1 表现断开毗连 * data 数据包携带的数据 */
- typedef struct {
- int type; // 0 for message, 1 for disconnect
- char data[BUFFER_SIZE];
- } Packet;/** * 主函数,客户端步调的入口点 * @return 步调的退出状态码,0 表现正常退出 */int main() { // client_fd 为客户端套接字文件形貌符 int client_fd; // server_addr 存储服务器的所在信息 struct sockaddr_in server_addr; // packet 用于存储要发送或吸取的数据包 Packet packet; // fds 数组用于存储要监听的文件形貌符及其变乱 struct pollfd fds[2]; // 创建客户端套接字,使用 IPv4 所在族和 TCP 协议 if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { // 若套接字创建失败,输堕落误信息并退出步调 perror("socket failed"); exit(EXIT_FAILURE); } // 设置服务器所在信息 server_addr.sin_family = AF_INET; // 将端标语从主机字节序转换为网络字节序 server_addr.sin_port = htons(PORT); // 将服务器的 IP 所在转换为网络字节序 server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 毗连到服务器 if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { // 若毗连失败,输堕落误信息,关闭套接字并退出步调 perror("connect"); close(client_fd); exit(EXIT_FAILURE); } // 输出毗连乐成的信息 printf("Connected to server\n"); // 初始化 poll 布局体 // 监听标准输入的可读变乱 fds[0].fd = STDIN_FILENO; fds[0].events = POLLIN; // 监听客户端套接字的可读变乱 fds[1].fd = client_fd; fds[1].events = POLLIN; // 进入无穷循环,一连处置处罚输入和服务器消息 while (1) { // 调用 poll 函数监听文件形貌符集会合的可读变乱,无超时时间 int activity = poll(fds, 2, -1); if ((activity < 0) && (errno != EINTR)) { // 若 poll 函数调用失败且不是被信号停止,输堕落误信息 perror("poll error"); } if (fds[0].revents & POLLIN) { // 若标准输入有可读变乱 // 清空数据包的数据部门 memset(packet.data, 0, BUFFER_SIZE); if (fgets(packet.data, BUFFER_SIZE, stdin) != NULL) { // 若乐成从标准输入读取数据 // 设置数据包范例为消息 packet.type = 0; // 发送数据包给服务器 send(client_fd, &packet, sizeof(Packet), 0); } } if (fds[1].revents & POLLIN) { // 若客户端套接字有可读变乱 // 清空数据包 memset(&packet, 0, sizeof(Packet)); if (read(client_fd, &packet, sizeof(Packet)) > 0) { // 若乐成从服务器读取数据 if (packet.type == 0) { // 若数据包范例为消息,输出吸取到的消息 printf("Received from server: %s", packet.data); } else if (packet.type == 1) { // 若数据包范例为断开毗连,输出服务器断开毗连的信息 printf("Server disconnected\n"); // 关闭客户端套接字 close(client_fd); // 跳出循环 break; } } else { // 若读取数据失败,阐明服务器断开毗连 printf("Server disconnected\n"); // 关闭客户端套接字 close(client_fd); // 跳出循环 break; } } } // 关闭客户端套接字 close(client_fd); return 0;}
复制代码 5.3客户端步调(client2.c)
- // 包罗标准输入输出库,用于使用 printf、perror 等函数举行输入输出操纵#include <stdio.h>// 包罗标准库,提供 exit 等函数用于步调退出等操纵#include <stdlib.h>// 包罗字符串处置处罚库,提供 memset、strlen 等字符串操纵函数#include <string.h>// 包罗 Unix 标准库,提供 close、read、write 等体系调用函数#include <unistd.h>// 包罗网络所在转换库,提供 inet_ntoa、htons 等网络所在转换函数#include <arpa/inet.h>// 包罗线程相干的头文件,用于创建和管理线程#include <pthread.h>// 界说服务器监听的端标语#define PORT 8080// 界说数据缓冲区的巨细#define BUFFER_SIZE 1024/** * 界说数据包布局体,用于在客户端和服务器之间传输数据 * type 数据包范例,0 表现消息,1 表现断开毗连 * data 数据包携带的数据 */
- typedef struct {
- int type; // 0 for message, 1 for disconnect
- char data[BUFFER_SIZE];
- } Packet;// 全局变量,存储客户端套接字文件形貌符int client_fd;/** * 线程函数,用于吸取服务器发送的消息 * arg 线程函数的参数,此处未使用 * @return 线程返回值,此处为 NULL */void *receive_messages(void *arg) { // 界说数据包变量,用于存储从服务器吸取的数据包 Packet packet; // 进入无穷循环,一连吸取服务器消息 while (1) { // 清空数据包 memset(&packet, 0, sizeof(Packet)); if (read(client_fd, &packet, sizeof(Packet)) > 0) { // 若乐成从服务器读取数据 if (packet.type == 0) { // 若数据包范例为消息,输出吸取到的消息 printf("Received from server: %s", packet.data); } else if (packet.type == 1) { // 若数据包范例为断开毗连,输出服务器断开毗连的信息 printf("Server disconnected\n"); // 关闭客户端套接字 close(client_fd); // 退出步调,返回失败状态 exit(EXIT_FAILURE); } } else { // 若读取数据失败,阐明服务器断开毗连 printf("Server disconnected\n"); // 关闭客户端套接字 close(client_fd); // 退出步调,返回失败状态 exit(EXIT_FAILURE); } } return NULL;}/** * 主函数,客户端步调的入口点 * @return 步调的退出状态码,0 表现正常退出 */int main() { // server_addr 存储服务器的所在信息 struct sockaddr_in server_addr; // packet 用于存储要发送的数据包 Packet packet; // thread_id 存储线程的标识符 pthread_t thread_id; // 创建客户端套接字,使用 IPv4 所在族和 TCP 协议 if ((client_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { // 若套接字创建失败,输堕落误信息并退出步调 perror("socket failed"); exit(EXIT_FAILURE); } // 设置服务器所在信息 server_addr.sin_family = AF_INET; // 将端标语从主机字节序转换为网络字节序 server_addr.sin_port = htons(PORT); // 将服务器的 IP 所在转换为网络字节序 server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 毗连到服务器 if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { // 若毗连失败,输堕落误信息,关闭套接字并退出步调 perror("connect"); close(client_fd); exit(EXIT_FAILURE); } // 输出毗连乐成的信息 printf("Connected to server\n"); // 创建线程来吸取消息 if (pthread_create(&thread_id, NULL, receive_messages, NULL) != 0) { // 若线程创建失败,输堕落误信息,关闭套接字并退出步调 perror("pthread_create"); close(client_fd); exit(EXIT_FAILURE); } // 进入无穷循环,一连从标准输入读取数据并发送给服务器 while (1) { // 清空数据包的数据部门 memset(packet.data, 0, BUFFER_SIZE); if (fgets(packet.data, BUFFER_SIZE, stdin) != NULL) { // 若乐成从标准输入读取数据 // 设置数据包范例为消息 packet.type = 0; // 发送数据包给服务器 send(client_fd, &packet, sizeof(Packet), 0); } } // 关闭客户端套接字 close(client_fd); return 0;}
复制代码 |