目录
一、TCP 服务器实现步骤
1.1 创建 socket
在 C 语言的网络编程中,socket()函数用于创建一个套接字,这个套接字就像是一个网络通信的 “端点”,为服务器与客户端之间的通信搭建了基础。其函数原型为:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- domain参数指定地址族,对于 IPv4,我们通常使用AF_INET;如果是 IPv6,则使用AF_INET6 。例如,在一个基于 IPv4 的 TCP 服务器中,domain的值会被设为AF_INET,表示使用 IPv4 地址进行通信。
- type参数确定套接字类型。对于面向连接的 TCP 协议,我们使用SOCK_STREAM;而对于无连接的 UDP 协议,则使用SOCK_DGRAM 。在 TCP 服务器场景下,type应设置为SOCK_STREAM,以确保数据传输的可靠性和有序性,就像打电话时,双方通过稳定的连接进行实时对话。
- protocol参数一般设为 0,让系统根据地址族和套接字类型自动选择合适的协议。在创建 TCP 套接字时,由于AF_INET和SOCK_STREAM组合已经明确指向 TCP 协议,所以protocol为 0 即可 。
创建服务器套接字的示例代码如下:
int server_socket;
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("socket creation failed");
// 处理错误,例如退出程序
exit(EXIT_FAILURE);
}
在这段代码中,socket()函数尝试创建一个 TCP 套接字。如果返回值为 - 1,说明创建过程中出现了错误,perror()函数会打印出具体的错误信息,帮助我们定位问题所在,然后程序通过exit(EXIT_FAILURE)退出,避免后续错误操作。
1.2 绑定地址
bind()函数的作用是将创建好的套接字与本地的 IP 地址和端口号进行绑定,使服务器能够在指定的地址和端口上监听连接请求,就像给一个商店确定具体的地理位置和门牌号,让顾客能够找到它。其函数原型为:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd是通过socket()函数创建的套接字描述符,它唯一标识了服务器的这个通信端点。
- addr是一个指向sockaddr结构体的指针,该结构体包含了要绑定的 IP 地址和端口号等信息。在 IPv4 中,我们通常使用sockaddr_in结构体来填充相关信息,然后将其强制转换为sockaddr类型 。例如:
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的本地IP地址
server_addr.sin_port = htons(8888); // 将端口号8888进行网络字节序转换后绑定
这里,sin_family设置为AF_INET表示 IPv4 地址族;sin_addr.s_addr设为INADDR_ANY,意味着服务器可以接受来自任何本地 IP 地址的连接,这在服务器可能有多个网络接口时非常实用;sin_port使用htons()函数将端口号 8888 从主机字节序转换为网络字节序后进行绑定,确保不同主机之间通信时端口号的一致性。
- addrlen参数是addr结构体的长度,对于sockaddr_in结构体,其长度可以通过sizeof(struct sockaddr_in)获取 。
绑定地址的完整代码示例如下:
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
// 处理错误,例如关闭套接字并退出程序
close(server_socket);
exit(EXIT_FAILURE);
}
如果bind()函数返回 - 1,说明绑定过程出现错误,perror()函数会输出错误信息,然后程序关闭套接字并退出,防止程序继续运行导致未知错误。
1.3 监听连接
listen()函数用于将套接字设置为监听状态,使其能够接收客户端的连接请求,就像商店老板打开店门,准备迎接顾客。其函数原型为:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- sockfd同样是之前创建并绑定好的套接字描述符。
- backlog参数定义了等待连接队列的最大长度,它决定了服务器在同一时间可以处理的未决连接数 。例如,当backlog设置为 10 时,服务器最多可以同时处理 10 个等待连接的客户端。
当客户端发起连接请求时,服务器会将这些请求放入等待连接队列中。如果队列已满,新的连接请求可能会被拒绝 。在实际应用中,backlog的设置需要根据服务器的负载和性能需求来调整。对于高并发的服务器,通常需要设置一个较大的backlog值,但也要考虑系统资源的限制,避免占用过多内存 。例如,在一个处理大量并发连接的 Web 服务器中,可能会将backlog设置为 1024 或更大,并结合系统参数的调整来优化性能 。
设置监听状态的示例代码如下:
if (listen(server_socket, 10) == -1) {
perror("listen failed");
// 处理错误,例如关闭套接字并退出程序
close(server_socket);
exit(EXIT_FAILURE);
}
若listen()函数返回 - 1,表明设置监听状态失败,程序会打印错误信息,关闭套接字并退出。
1.4 接受连接
accept()函数用于接收客户端的连接请求,并返回一个新的套接字,这个新套接字专门用于与该客户端进行通信,就像商店老板安排一个员工专门为进店的顾客服务。其函数原型为:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd是处于监听状态的套接字描述符。
- addr是一个指向sockaddr结构体的指针,用于存储客户端的地址信息,通过这个参数,服务器可以获取客户端的 IP 地址和端口号 。
- addrlen是一个指向socklen_t类型的变量,用于传入和返回addr结构体的长度 。在调用accept()之前,需要将addrlen初始化为addr结构体的大小;函数返回时,addrlen会被更新为实际接收到的客户端地址结构体的长度 。
accept()函数会阻塞当前进程,直到有客户端连接到来 。当有客户端连接时,它从等待连接队列中取出一个连接请求,并创建一个新的套接字用于与该客户端通信 。这个新套接字与监听套接字不同,监听套接字继续保持监听状态,等待其他客户端的连接,而新套接字则负责与特定客户端进行数据交互 。
接受客户端连接的示例代码如下:
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_socket == -1) {
perror("accept failed");
// 处理错误,例如继续等待下一个连接或关闭服务器
return;
}
在这段代码中,accept()函数尝试接受客户端连接。如果返回 - 1,说明接受连接失败,程序会打印错误信息,然后根据实际需求进行错误处理,比如继续等待下一个连接请求,或者直接关闭服务器。如果接受成功,client_socket将成为与客户端通信的新套接字,后续就可以通过它与客户端进行数据的收发操作。
二、服务器数据交互实战
2.1 接收数据
在 TCP 服务器中,使用recv()函数来接收客户端发送的数据。recv()函数从指定的套接字接收数据,并将其存储到缓冲区中。其函数原型为:
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- sockfd是用于接收数据的套接字描述符,它标识了与客户端通信的连接。
- buf是一个指向缓冲区的指针,接收到的数据将被存储在这个缓冲区中。
- len指定了缓冲区的大小,即最多可以接收的数据字节数 。
- flags参数用于指定接收数据的方式,通常设置为 0,表示默认的接收方式 。如果需要特殊的接收行为,例如设置为MSG_WAITALL,表示等待直到接收到指定长度的数据,但这种设置可能会导致在网络状况不佳时进程长时间阻塞。
接收客户端数据并输出到控制台的示例代码如下:
char buffer[1024];
ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0'; // 添加字符串结束符,确保输出正确
printf("Received from client: %s\n", buffer);
} else if (bytes_received == 0) {
printf("Client closed the connection.\n");
} else {
perror("recv failed");
}
在这段代码中,首先定义了一个大小为 1024 字节的缓冲区buffer,用于存储接收到的数据。然后调用recv()函数从client_socket套接字接收数据,将接收到的字节数存储在bytes_received变量中。如果接收到的数据长度大于 0,说明成功接收到数据,在数据末尾添加字符串结束符’\0’,然后通过printf()函数将数据输出到控制台。如果bytes_received为 0,表示客户端关闭了连接;如果返回值小于 0,则说明接收数据时发生了错误,使用perror()函数打印错误信息 。
2.2 发送数据
send()函数用于向已连接的套接字发送数据,将服务器的反馈信息传递给客户端。其函数原型为:
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- sockfd是要发送数据的目标套接字描述符。
- buf指向包含要发送数据的缓冲区。
- len指定要发送的数据的字节数 。
- flags参数同样用于指定发送数据的特殊方式,通常设为 0 。例如,如果设置为MSG_OOB,表示发送带外数据,这在一些特殊的网络通信场景中可能会用到。
向客户端发送反馈数据 “Received: Hello Server!” 的示例代码如下:
const char *response = "Received: Hello Server!";
ssize_t bytes_sent = send(client_socket, response, strlen(response), 0);
if (bytes_sent == -1) {
perror("send failed");
}
在这段代码中,定义了一个常量字符串response作为要发送给客户端的反馈信息。然后调用send()函数,将response中的数据通过client_socket发送给客户端。如果send()函数返回 - 1,表示发送数据时出现错误,使用perror()函数打印错误信息。
2.3 多客户端处理(简易版)
在实际应用中,服务器通常需要处理多个客户端的连接。一种简单的实现方式是使用循环来不断接受新的客户端连接,然后逐个处理每个客户端的请求 。在处理一个客户端连接时,服务器可以为该客户端创建一个新的线程或进程,也可以在主线程中顺序处理 。下面是一个在主线程中顺序处理多个客户端连接的示例代码:
while (1) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_socket == -1) {
perror("accept failed");
continue;
}
// 处理当前客户端连接
// 例如接收和发送数据
char buffer[1024];
ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("Received from client: %s\n", buffer);
const char *response = "Received: Hello Server!";
ssize_t bytes_sent = send(client_socket, response, strlen(response), 0);
if (bytes_sent == -1) {
perror("send failed");
}
} else if (bytes_received == 0) {
printf("Client closed the connection.\n");
} else {
perror("recv failed");
}
close(client_socket); // 关闭当前客户端连接,准备接受下一个
}
在这个示例中,通过一个无限循环while(1),服务器不断调用accept()函数来接受新的客户端连接。如果accept()成功,返回一个新的套接字client_socket用于与该客户端通信 。接着在循环内部处理当前客户端的请求,包括接收数据、处理数据并发送反馈,处理完成后关闭该客户端的套接字,然后继续循环接受下一个客户端连接 。这种方式虽然简单,但在处理高并发场景时可能会存在性能瓶颈,因为它是顺序处理每个客户端连接,当一个客户端的处理时间较长时,会影响其他客户端的响应速度 。在实际应用中,对于高并发场景,通常会采用多线程、多进程或异步 I/O 等技术来提高服务器的并发处理能力。
三、服务器代码编写与测试
3.1 编写完整服务器代码
下面是一个完整的 TCP 服务器代码示例,按照初始化、绑定、监听、接受、交互、关闭的步骤详细注释:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#define PORT 8888 // 定义服务器监听的端口号
#define MAX_CLIENTS 5 // 定义最大等待连接数
#define BUFFER_SIZE 1024 // 定义缓冲区大小
int main() {
int server_socket, client_socket;
struct sockaddr_in server_address, client_address;
socklen_t client_address_len = sizeof(client_address);
char buffer[BUFFER_SIZE];
// 1. 初始化:创建套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 绑定地址
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的本地IP地址
server_address.sin_port = htons(PORT); // 将端口号进行网络字节序转换
if (bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
perror("bind failed");
close(server_socket);
exit(EXIT_FAILURE);
}
// 3. 监听连接
if (listen(server_socket, MAX_CLIENTS) == -1) {
perror("listen failed");
close(server_socket);
exit(EXIT_FAILURE);
}
printf("Server is listening on port %d...\n", PORT);
while (1) {
// 4. 接受连接
client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_address_len);
if (client_socket == -1) {
perror("accept failed");
continue;
}
printf("Accepted connection from client.\n");
// 5. 数据交互
// 接收数据
ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0'; // 添加字符串结束符
printf("Received from client: %s\n", buffer);
// 发送数据
const char *response = "Received: Hello Server!";
ssize_t bytes_sent = send(client_socket, response, strlen(response), 0);
if (bytes_sent == -1) {
perror("send failed");
}
} else if (bytes_received == 0) {
printf("Client closed the connection.\n");
} else {
perror("recv failed");
}
// 6. 关闭当前客户端连接,准备接受下一个
close(client_socket);
}
// 关闭服务器套接字(正常情况下不会执行到这里,因为上面是无限循环)
close(server_socket);
return 0;
}
3.2 测试
- 启动服务器:在终端中,使用gcc编译上述代码,例如:gcc -o server server.c(假设代码保存为server.c),然后运行生成的可执行文件./server。此时,服务器将开始监听指定的端口(如 8888),并输出 “Server is listening on port 8888…”。
- 连接客户端:参考第 53 篇文章中的 TCP 客户端代码,同样进行编译和运行。假设客户端代码保存为client.c,编译命令为gcc -o client client.c,运行命令为./client(客户端代码中需要指定正确的服务器 IP 地址和端口号,确保与服务器设置一致)。
- 验证数据双向交互:在客户端输入数据并发送,服务器应能接收到数据并输出到控制台,同时服务器会向客户端发送反馈信息 “Received: Hello Server!”。客户端也应能接收到服务器的反馈,并显示在控制台上,从而验证数据的双向交互功能。
3.3 错误处理
- 绑定失败(端口被占用):当bind()函数返回 - 1 时,可能是因为指定的端口已被其他程序占用。处理方法如下:
- 检查端口占用情况:在 Linux 系统中,可以使用netstat -ano | grep <端口号>命令查看指定端口的占用情况,找出占用端口的进程 ID(PID)。例如,要检查 8888 端口的占用情况,命令为netstat -ano | grep 8888。
- 关闭占用端口的进程:根据查到的 PID,使用kill -9 命令强制关闭占用端口的进程(kill -9是强制终止进程的信号,使用时需谨慎,确保关闭的是正确的进程)。例如,如果 PID 为 1234,命令为kill -9 1234。
- 重新启动服务器:在关闭占用端口的进程后,重新启动服务器程序,使其能够成功绑定到指定端口。
- 接收数据超时:在recv()函数接收数据时,如果网络状况不佳或客户端没有及时发送数据,可能会导致接收超时。可以通过设置套接字选项来处理这种情况:
- 设置接收超时时间:使用setsockopt()函数设置SO_RCVTIMEO选项来指定接收数据的超时时间。例如,设置超时时间为 5 秒:
struct timeval timeout;
timeout.tv_sec = 5; // 5秒
timeout.tv_usec = 0;
if (setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof(timeout)) < 0) {
perror("setsockopt failed");
}
- 处理接收超时错误:在调用recv()函数后,检查返回值和errno。如果recv()返回 - 1 且errno为EAGAIN或EWOULDBLOCK,表示接收超时。例如:
ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer) - 1, 0);
if (bytes_received == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("Receive timed out.\n");
// 可以选择继续等待、关闭连接或进行其他处理
} else {
perror("recv failed");
}
}
这样,通过合理的错误处理机制,可以使 TCP 服务器更加健壮和稳定,能够应对各种网络异常情况。
2407

被折叠的 条评论
为什么被折叠?



