一、概述
本文档针对 TCP 客户端程序和 TCP 服务器程序。客户端程序会连接到服务器并发送带有自定义协议格式的数据,而服务器程序则负责监听客户端连接,接收并处理这些数据。自定义协议格式为:先发送 2 字节网络字节序的长度头,随后是变长的数据负载。
二、客户端程序
2.1 代码结构
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8080 // 定义服务器监听的端口号
#define SERVER_IP "127.0.0.1" // 定义服务器的 IP 地址
// 发送协议包
void send_packet(int client_socket, const char *data) {
uint16_t packet_length = strlen(data); // 获取数据长度
packet_length = htons(packet_length); // 将数据长度转换为网络字节序
// 发送协议包长度
if (write(client_socket, &packet_length, sizeof(packet_length)) != sizeof(packet_length)) {
perror("write packet length failed"); // 如果发送失败,打印错误信息
return;
}
// 发送协议包数据
if (write(client_socket, data, ntohs(packet_length)) != ntohs(packet_length)) {
perror("write packet data failed"); // 如果发送失败,打印错误信息
return;
}
}
int main() {
int client_socket; // 客户端套接字描述符
struct sockaddr_in server_addr; // 存储服务器地址信息的结构体
// 创建客户端套接字
if ((client_socket = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation error"); // 如果创建失败,打印错误信息
return -1; // 返回错误码
}
server_addr.sin_family = AF_INET; // 设置地址族为 IPv4
server_addr.sin_port = htons(PORT); // 设置服务器监听的端口号,并转换为网络字节序
// 将点分十进制的 IP 地址转换为二进制形式
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported"); // 如果转换失败,打印错误信息
return -1; // 返回错误码
}
// 连接到服务器
if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Connection Failed"); // 如果连接失败,打印错误信息
return -1; // 返回错误码
}
// 要发送的数据
const char *message = "Hello, server!";
// 发送协议包
send_packet(client_socket, message);
// 关闭套接字
close(client_socket);
return 0; // 正常退出
}
2.2 功能模块
宏定义:
PORT
:设定服务器端口号为 8080。SERVER_IP
:指定服务器 IP 地址为本地回环地址 127.0.0.1。
send_packet
函数:- 该函数的作用是按自定义协议格式发送数据。
- 先计算数据长度,接着将其转换为网络字节序,再发送长度头,最后发送数据负载。
main
函数:- 创建 TCP 套接字。
- 对服务器地址结构进行设置。
- 连接到服务器。
- 调用
send_packet
函数发送数据。 - 关闭套接字。
2.3 编译与运行
- 编译:运用
gcc client.c -o client
命令进行编译。 - 运行:执行
./client
命令。运行前要确保服务器已启动。
三、服务器程序
3.1 代码结构
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8080 // 定义服务器监听的端口号
#define BACKLOG 5 // 定义监听队列的最大长度
// 读取指定长度的数据
ssize_t read_n(int fd, void *buf, size_t n) {
size_t total_read = 0; // 记录已读取的总字节数
ssize_t read_bytes; // 每次读取的实际字节数
while (total_read < n) { // 循环直到读取到指定长度的数据
read_bytes = read(fd, (char *)buf + total_read, n - total_read);
if (read_bytes <= 0) { // 如果读取失败或连接关闭
return read_bytes; // 返回读取结果
}
total_read += read_bytes; // 更新已读取的总字节数
}
return total_read; // 返回实际读取的总字节数
}
// 处理协议包
void handle_packet(int client_socket) {
uint16_t packet_length; // 协议包长度(网络字节序)
// 读取协议包长度
if (read_n(client_socket, &packet_length, sizeof(packet_length)) != sizeof(packet_length)) {
perror("read packet length failed"); // 如果读取失败,打印错误信息
return;
}
packet_length = ntohs(packet_length); // 将网络字节序转换为主机字节序
// 分配内存来存储协议包数据
char *packet_data = (char *)malloc(packet_length);
if (packet_data == NULL) { // 如果内存分配失败
perror("malloc failed"); // 打印错误信息
return;
}
// 读取协议包数据
if (read_n(client_socket, packet_data, packet_length) != packet_length) {
perror("read packet data failed"); // 如果读取失败,打印错误信息
free(packet_data); // 释放分配的内存
return;
}
// 打印接收到的协议包内容
printf("Received packet of length %d: %.*s\n", packet_length, (int)packet_length, packet_data);
// 释放内存
free(packet_data);
}
int main() {
int server_fd; // 服务器套接字描述符
int new_socket; // 新连接的客户端套接字描述符
struct sockaddr_in address; // 存储服务器地址信息的结构体
int opt = 1; // 设置套接字选项的值
int addrlen = sizeof(address); // 地址结构体的长度
// 创建服务器套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed"); // 如果创建失败,打印错误信息
exit(EXIT_FAILURE); // 异常退出
}
// 设置套接字选项,允许地址重用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt"); // 如果设置失败,打印错误信息
exit(EXIT_FAILURE); // 异常退出
}
// 准备 IP 和 port
address.sin_family = AF_INET; // 设置地址族为 IPv4
address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
address.sin_port = htons(PORT); // 设置监听的端口号,并转换为网络字节序
// 绑定 IP 和 port
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed"); // 如果绑定失败,打印错误信息
exit(EXIT_FAILURE); // 异常退出
}
// 创建监听列表
if (listen(server_fd, BACKLOG) < 0) {
perror("listen"); // 如果监听失败,打印错误信息
exit(EXIT_FAILURE); // 异常退出
}
printf("Server listening on port %d...\n", PORT); // 提示服务器正在监听
// 进入无限循环,持续接受客户端连接请求
while (1) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept"); // 如果接受连接失败,打印错误信息
continue; // 跳过本次循环,继续等待下一个连接
}
// 处理客户端发送的协议包
handle_packet(new_socket);
// 关闭与客户端的连接
close(new_socket);
}
return 0; // 正常退出
}
3.2 功能模块
- 宏定义:
PORT
:设定服务器监听端口号为 8080。BACKLOG
:设置监听队列的最大长度为 5。
read_n
函数:- 该函数的作用是读取指定长度的数据,会处理可能出现的多次读取情况。
handle_packet
函数:- 读取协议包长度并转换为主机字节序。
- 分配内存以存储数据负载。
- 读取数据负载。
- 打印接收到的数据信息。
- 释放内存。
main
函数:- 创建 TCP 套接字。
- 设置套接字选项,允许地址重用。
- 绑定 IP 地址和端口号。
- 将套接字设置为监听状态。
- 进入无限循环,持续接受客户端连接。
- 调用
handle_packet
函数处理接收到的数据。 - 关闭与客户端的连接。
3.3 编译与运行
- 编译:使用
gcc server.c -o server
命令进行编译。 - 运行:执行
./server
命令。运行后,服务器会开始监听指定端口。