Socket编程实战:从基础API到多线程服务器
一、Socket编程概述:网络通信的桥梁
Socket(套接字)是网络通信的端点,它提供了不同主机间进程通信的接口。在Linux系统中,Socket可以被视为一种特殊的文件描述符,通过标准的文件I/O操作来进行网络数据传输。
Socket编程的核心概念
通信域:确定通信的协议族和地址格式
套接字类型:定义通信的语义和特性
协议:指定具体的传输协议
地址:标识网络中的通信端点
学习Socket编程的重要性:
网络应用开发的基础
理解网络协议的实现原理
系统编程能力的重要体现
二、socket()函数详解:创建通信端点
socket()函数是Socket编程的起点,用于创建通信端点。
函数原型
int socket(int domain, int type, int protocol);参数详解表
| 参数 | 类型 | 说明 | 常用值 | 示例值含义 |
|---|---|---|---|---|
| domain | int | 协议族/地址族 | AF_INET, AF_INET6, AF_UNIX | AF_INET: IPv4网络通信 |
| type | int | 套接字类型 | SOCK_STREAM, SOCK_DGRAM | SOCK_STREAM: TCP流式套接字 |
| protocol | int | 具体协议 | 0, IPPROTO_TCP, IPPROTO_UDP | 0: 根据前两个参数自动选择 |
常用参数组合
TCP套接字:
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);UDP套接字:
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);本地套接字:
int local_socket = socket(AF_UNIX, SOCK_STREAM, 0);返回值说明
成功:返回非负的文件描述符 失败:返回-1,并设置errno
记忆技巧:将socket()参数记为"族型协"——协议族、套接字类型、协议。
三、bind()函数与sockaddr_in结构体
bind()函数将套接字与特定的IP地址和端口号关联起来。
函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockaddr_in结构体详解
struct sockaddr_in {sa_family_t sin_family; // 地址族,如AF_INETin_port_t sin_port; // 端口号(网络字节序)struct in_addr sin_addr; // IP地址(网络字节序)unsigned char sin_zero[8]; // 填充字段,通常设为0
};struct in_addr {uint32_t s_addr; // IPv4地址
};地址设置示例
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 端口8080
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意网络接口bind()使用示例
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {perror("bind failed");exit(EXIT_FAILURE);
}关键点:
需要将sockaddr_in强制转换为sockaddr*
端口和地址必须使用网络字节序(大端序)
INADDR_ANY表示绑定到所有可用的网络接口
四、listen()与accept()函数详解
listen()函数
listen()将套接字转换为被动套接字,开始监听连接请求。
int listen(int sockfd, int backlog);参数说明:
sockfd:已绑定的套接字描述符
backlog:连接请求队列的最大长度
backlog参数详解:
表示已完成三次握手但尚未被accept()接受的连接数
典型值:5-10(根据服务器负载能力调整)
过大:浪费内核资源;过小:可能导致连接被拒绝
accept()函数
accept()从监听队列中接受连接,创建新的连接套接字。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);参数说明:
sockfd:监听套接字描述符
addr:输出参数,存储客户端地址信息
addrlen:输入输出参数,指定地址结构大小
使用示例
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// ... bind操作 ...if (listen(server_fd, 5) < 0) {perror("listen failed");exit(EXIT_FAILURE);
}printf("Server listening on port 8080...\n");struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {perror("accept failed");exit(EXIT_FAILURE);
}printf("Client connected from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));五、监听套接字 vs 连接套接字
理解两种套接字的区别对于编写正确的服务器程序至关重要。
对比分析表
| 特性 | 监听套接字 | 连接套接字 |
|---|---|---|
| 创建方式 | socket()创建 | accept()返回 |
| 角色 | 被动接受连接 | 主动数据通信 |
| 生命周期 | 整个服务器运行期间 | 单个连接期间 |
| 数量 | 通常1个 | 每个客户端连接1个 |
| 操作 | bind(), listen(), accept() | send(), recv(), close() |
| 端口使用 | 绑定特定端口 | 使用相同端口但不同描述符 |
套接字状态转换
socket() → bind() → listen() → accept() → 新连接套接字↑ ↑ ↑ ↑ 监听套接字 绑定地址 开始监听 接受连接
重要概念:监听套接字只负责接受新连接,不用于数据传输。每个接受的连接都会创建一个新的连接套接字专门处理该客户端通信。
六、connect()、getpeername()、close()等函数使用
connect()函数
客户端使用connect()主动连接服务器。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);使用示例:
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {perror("connect failed");exit(EXIT_FAILURE);
}
printf("Connected to server\n");getpeername()函数
获取对端套接字的地址信息。
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);使用场景:
服务器想知道客户端的详细信息
调试和日志记录
安全审计
close()函数
关闭套接字,释放资源。
int close(int fd);重要提示:
关闭套接字会触发TCP连接终止序列
服务器需要关闭监听套接字和所有连接套接字
关闭后文件描述符不再有效
完整函数参考表
| 函数 | 头文件 | 参数说明 | 返回值 | 主要用途 |
|---|---|---|---|---|
| socket() | <sys/socket.h> | domain, type, protocol | 套接字描述符 | 创建通信端点 |
| bind() | <sys/socket.h> | sockfd, addr, addrlen | 0成功/-1失败 | 绑定地址到套接字 |
| listen() | <sys/socket.h> | sockfd, backlog | 0成功/-1失败 | 开始监听连接 |
| accept() | <sys/socket.h> | sockfd, addr, addrlen | 新套接字描述符 | 接受客户端连接 |
| connect() | <sys/socket.h> | sockfd, addr, addrlen | 0成功/-1失败 | 连接服务器 |
| getpeername() | <sys/socket.h> | sockfd, addr, addrlen | 0成功/-1失败 | 获取对端地址 |
| close() | <unistd.h> | fd | 0成功/-1失败 | 关闭套接字 |
七、多线程与pthread_detach()
多线程服务器可以同时处理多个客户端连接,提高服务器并发能力。
pthread_create()创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);pthread_detach()分离线程
设置线程为分离状态,线程结束后自动释放资源。
int pthread_detach(pthread_t thread);为什么使用pthread_detach()?
分离线程不需要主线程调用pthread_join()等待
线程结束后自动回收资源
避免内存泄漏
多线程服务器示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
void* handle_client(void* arg) {int client_fd = *(int*)arg;free(arg); // 释放动态分配的内存char buffer[BUFFER_SIZE];ssize_t bytes_read;pthread_detach(pthread_self()); // 分离线程,自动回收资源while ((bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0)) > 0) {buffer[bytes_read] = '\0';printf("Received: %s", buffer);send(client_fd, buffer, bytes_read, 0); // 回显数据}close(client_fd);printf("Client disconnected\n");return NULL;
}
int main() {int server_fd, *client_fd;struct sockaddr_in address;socklen_t addrlen = sizeof(address);pthread_t thread_id;if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}if (listen(server_fd, 10) < 0) {perror("listen");exit(EXIT_FAILURE);}printf("Multi-threaded server listening on port %d...\n", PORT);while (1) {client_fd = malloc(sizeof(int)); // 为每个客户端动态分配内存if (!client_fd) {perror("malloc failed");continue;}*client_fd = accept(server_fd, (struct sockaddr*)&address, &addrlen);if (*client_fd < 0) {perror("accept");free(client_fd);continue;}printf("New client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));if (pthread_create(&thread_id, NULL, handle_client, client_fd) != 0) {perror("pthread_create");close(*client_fd);free(client_fd);}}close(server_fd);return 0;
}八、完整代码示例:TCP服务器与客户端
TCP服务器完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
void handle_connection(int client_fd, struct sockaddr_in client_addr) {char buffer[BUFFER_SIZE];ssize_t bytes_read;printf("Handling client %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));while ((bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0)) > 0) {buffer[bytes_read] = '\0';printf("From %s:%d: %s", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);if (send(client_fd, buffer, bytes_read, 0) < 0) {perror("send failed");break;}if (strncmp(buffer, "quit", 4) == 0) {printf("Client %s:%d requested to quit\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));break;}}close(client_fd);printf("Client %s:%d disconnected\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
}
int main() {int server_fd, client_fd;struct sockaddr_in server_addr, client_addr;socklen_t client_len = sizeof(client_addr);if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}int opt = 1;if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {perror("setsockopt");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);}if (listen(server_fd, MAX_CLIENTS) < 0) {perror("listen failed");close(server_fd);exit(EXIT_FAILURE);}printf("TCP Echo Server listening on port %d...\n", PORT);printf("Server will echo back received messages. Send 'quit' to disconnect.\n");while (1) {client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);if (client_fd < 0) {perror("accept failed");continue;}handle_connection(client_fd, client_addr);}close(server_fd);return 0;
}TCP客户端完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024
int main() {int sockfd;struct sockaddr_in server_addr;char buffer[BUFFER_SIZE];ssize_t bytes_sent, bytes_received;if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {perror("invalid address");close(sockfd);exit(EXIT_FAILURE);}if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {perror("connection failed");close(sockfd);exit(EXIT_FAILURE);}printf("Connected to server %s:%d\n", SERVER_IP, SERVER_PORT);printf("Type messages to send to server (type 'quit' to exit):\n");while (1) {printf("Client: ");if (!fgets(buffer, sizeof(buffer), stdin)) {break;}bytes_sent = send(sockfd, buffer, strlen(buffer), 0);if (bytes_sent < 0) {perror("send failed");break;}if (strncmp(buffer, "quit", 4) == 0) {printf("Disconnecting...\n");break;}bytes_received = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (bytes_received < 0) {perror("recv failed");break;} else if (bytes_received == 0) {printf("Server closed connection\n");break;}buffer[bytes_received] = '\0';printf("Server: %s", buffer);}close(sockfd);printf("Connection closed\n");return 0;
}九、常见面试题精析
1. socket()函数中AF_INET和PF_INET有什么区别?
参考答案:
AF_INET表示地址族(Address Family),PF_INET表示协议族(Protocol Family)。在大多数实现中,它们被定义为相同的值,可以互换使用。但严格来说:
AF_xxx用于地址结构(如sockaddr_in)
PF_xxx用于协议族(如socket()的第一个参数)
2. TCP服务器为什么需要调用listen()函数?
参考答案:
listen()函数将主动套接字转换为被动套接字,使其能够接受连接请求。具体作用:
创建连接请求队列
设置同时等待处理的最大连接数
使套接字进入监听状态
3. accept()函数返回的套接字和监听套接字有什么区别?
参考答案:
监听套接字:用于接受新连接,生命周期与服务器相同
连接套接字:用于与特定客户端通信,每个客户端连接一个
监听套接字绑定到固定端口,连接套接字使用相同端口但不同文件描述符
4. 什么是字节序?网络编程中为什么要处理字节序?
参考答案:
字节序是多字节数据在内存中的存储顺序:
大端序:高位字节存储在低地址
小端序:低位字节存储在低地址
网络编程使用网络字节序(大端序)保证不同架构主机间的兼容性。需要使用htons()、htonl()、ntohs()、ntohl()进行转换。
5. 多线程服务器中为什么建议使用pthread_detach()?
参考答案:
分离线程结束后自动释放资源,避免内存泄漏
不需要主线程调用pthread_join()等待线程结束
提高资源利用率,避免僵尸线程
简化线程管理代码
6. 如何优雅地关闭套接字连接?
参考答案:
服务器端:先close()连接套接字,最后close()监听套接字
客户端:调用close()关闭连接
可以使用shutdown()控制关闭方向:
SHUT_RD:关闭读端 SHUT_WR:关闭写端 SHUT_RDWR:完全关闭
7. 什么是SO_REUSEADDR选项?有什么作用?
参考答案:
SO_REUSEADDR允许套接字绑定到处于TIME_WAIT状态的地址。作用:
服务器重启后可以立即重用相同端口
避免"Address already in use"错误
提高服务器可用性
8. 如何实现一个并发服务器?
参考答案:
几种常见的并发模型:
多进程:fork()创建子进程处理连接
多线程:pthread_create()创建线程处理连接
I/O多路复用:select()/poll()/epoll()监控多个套接字
异步I/O:aio_*系列函数
9. TCP的粘包问题如何解决?
参考答案:
固定长度:所有消息采用相同长度
长度前缀:在消息前添加长度字段
分隔符:使用特殊字符作为消息边界
协议设计:设计自描述的应用层协议
10. 什么是非阻塞I/O?如何设置非阻塞套接字?
参考答案:
非阻塞I/O使操作立即返回,不等待操作完成。
设置方法:
#include <fcntl.h>
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);总结
Socket编程是网络应用开发的核心技术,通过本文的学习,你应该掌握:
核心API理解:socket()、bind()、listen()、accept()等函数的作用和参数
套接字类型区分:监听套接字与连接套接字的不同角色
地址处理能力:sockaddr_in结构体的使用和字节序转换
并发编程技巧:多线程服务器的实现和线程管理
完整项目实践:TCP服务器和客户端的完整实现
面试准备充分:常见Socket编程面试题的解答
进阶学习建议:
学习I/O多路复用(select/poll/epoll)
掌握非阻塞I/O和异步编程
理解网络协议底层原理
实践实际项目,如HTTP服务器、聊天程序等
