面试题答案
一键面试数据结构设计
- 连接管理:
- 使用链表或数组来管理所有的TCP连接。链表灵活性高,便于动态添加和删除连接;数组访问效率高,适用于连接数量相对固定的场景。例如,定义一个结构体数组:
struct connection { int sockfd; struct sockaddr_in server_addr; // 用于标记连接状态,如连接中、传输中、已关闭等 int status; // 用于大文件传输时记录已传输字节数 off_t transferred_bytes; }; struct connection connections[MAX_CONNECTIONS];
- 缓冲区管理:
- 为每个连接分配读、写缓冲区。对于大文件传输,可以使用较大的缓冲区以减少系统调用次数。可以采用环形缓冲区(ring buffer)来处理数据的读写,它能有效利用内存空间且支持高效的读写操作。例如:
struct ring_buffer { char *buffer; size_t capacity; size_t read_index; size_t write_index; };
- 对于大文件传输,还可以考虑使用内存映射文件(
mmap
),将文件映射到内存,直接通过内存操作来传输数据,减少数据在用户空间和内核空间之间的拷贝。
算法设计
- 多线程/多进程模型:
- 多线程:每个连接对应一个线程,线程之间共享进程资源,通过互斥锁(
pthread_mutex_t
)等同步机制来保护共享资源。例如,在访问共享的连接状态或缓冲区时,需要先获取锁。
pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); // 在访问共享资源前 pthread_mutex_lock(&mutex); // 访问共享资源 pthread_mutex_unlock(&mutex);
- 多进程:使用
fork
创建子进程,每个子进程处理一个连接。进程间通信可以使用管道(pipe
)或套接字(socketpair)。例如,父进程可以通过管道向子进程发送控制信息,如关闭连接的指令。
- 多线程:每个连接对应一个线程,线程之间共享进程资源,通过互斥锁(
- 事件驱动模型:
- 使用
epoll
(在Linux系统下)实现事件驱动。epoll
可以高效地管理大量的文件描述符,并且只通知有事件发生的描述符。在epoll
的回调函数中处理连接的读写事件。例如:
int epollfd = epoll_create1(0); struct epoll_event event; event.data.fd = sockfd; event.events = EPOLLIN | EPOLLOUT | EPOLLET; // 边缘触发模式 epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &event); struct epoll_event events[MAX_EVENTS]; int nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); for (int i = 0; i < nfds; ++i) { if (events[i].events & EPOLLIN) { // 处理读事件 } else if (events[i].events & EPOLLOUT) { // 处理写事件 } }
- 使用
避免常见网络编程陷阱
- 连接超时:
- 设置连接超时时间。在调用
connect
函数前,使用setsockopt
设置SO_SNDTIMEO
和SO_RCVTIMEO
选项。例如:
struct timeval timeout; timeout.tv_sec = 5; // 5秒超时 timeout.tv_usec = 0; setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&timeout, sizeof(timeout)); setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof(timeout));
- 设置连接超时时间。在调用
- 数据传输完整性:
- 在发送和接收数据时,使用循环发送和接收,确保数据完整传输。例如,发送数据时:
ssize_t total_sent = 0; while (total_sent < data_size) { ssize_t sent = send(sockfd, data + total_sent, data_size - total_sent, 0); if (sent == -1) { // 处理错误 break; } total_sent += sent; }
- 接收数据时,同样使用循环接收,直到预期的数据量接收完毕。
- 内存管理:
- 动态分配的内存(如缓冲区)要及时释放,避免内存泄漏。在关闭连接时,要确保所有相关的内存都已释放。例如,对于环形缓冲区:
free(ring_buffer.buffer);
- 错误处理:
- 对每个系统调用(如
connect
、send
、recv
等)的返回值进行检查,及时处理错误。例如:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket"); // 处理错误,如退出程序或尝试重新创建 }
- 对每个系统调用(如