1. 套接字基础概念与网络通信模型
套接字(Socket)是Linux网络编程中最核心的概念之一,它本质上是一个通信端点,可以理解为网络数据传输的"插座"。在实际开发中,我们通过创建套接字来建立网络连接、发送和接收数据。
1.1 套接字的基本组成
一个完整的套接字由以下几个关键要素组成:
- IP地址:标识网络中的主机(如192.168.1.1)
- 传输协议:TCP或UDP
- 端口号:标识主机上的具体服务(如80端口对应HTTP服务)
这三者的组合可以表示为:套接字 = IP地址 + 传输协议 + 端口号。在网络通信中,套接字总是成对出现的——服务器端套接字和客户端套接字。
1.2 套接字通信的基本流程
典型的套接字通信遵循以下步骤:
服务器端:
- 创建套接字(socket())
- 绑定IP和端口(bind())
- 开始监听(listen())
- 接受连接(accept())
- 收发数据(read()/write())
- 关闭连接(close())
客户端:
- 创建套接字(socket())
- 连接服务器(connect())
- 收发数据(read()/write())
- 关闭连接(close())
这个流程是TCP套接字通信的基础模型,也是我们后续要详细讲解的核心内容。
1.3 套接字的类型
Linux系统中主要支持以下几种套接字类型:
流式套接字(SOCK_STREAM):
- 基于TCP协议
- 提供可靠的、面向连接的字节流服务
- 保证数据按序到达,无差错
数据报套接字(SOCK_DGRAM):
- 基于UDP协议
- 提供无连接的数据报服务
- 不保证可靠传输,但效率更高
原始套接字(SOCK_RAW):
- 允许直接访问底层协议
- 可以构造自定义协议头
- 通常用于网络诊断或特殊协议实现
在实际开发中,我们最常用的是前两种类型,特别是SOCK_STREAM类型的TCP套接字。
2. TCP套接字编程详解
TCP套接字是网络编程中最常用的类型,它提供了可靠的、面向连接的通信服务。下面我们将深入讲解TCP套接字编程的各个环节。
2.1 创建套接字
在Linux中,我们使用socket()系统调用来创建套接字:
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);参数说明:
- domain:协议族,常用AF_INET(IPv4)或AF_INET6(IPv6)
- type:套接字类型,SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)
- protocol:通常设为0,由系统自动选择
示例代码:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); }2.2 绑定地址和端口
服务器端需要将套接字绑定到特定的IP地址和端口上,使用bind()函数:
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);其中,sockaddr_in结构体用于存储地址信息:
struct sockaddr_in { sa_family_t sin_family; /* 地址族: AF_INET */ in_port_t sin_port; /* 端口号 */ struct in_addr sin_addr; /* IP地址 */ }; struct in_addr { uint32_t s_addr; /* 32位IP地址 */ };示例代码:
struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有本地IP servaddr.sin_port = htons(8080); // 绑定8080端口 if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); }注意:端口号需要使用htons()函数转换为网络字节序,IP地址使用htonl()函数转换。INADDR_ANY表示绑定所有可用的本地IP地址。
2.3 监听连接
服务器端使用listen()函数开始监听连接请求:
#include <sys/socket.h> int listen(int sockfd, int backlog);参数说明:
- sockfd:已绑定的套接字描述符
- backlog:等待连接队列的最大长度
示例代码:
if (listen(sockfd, 5) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } printf("Server listening on port 8080...\n");2.4 接受连接
服务器端使用accept()函数接受客户端连接:
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);这个函数会阻塞,直到有客户端连接到来。它返回一个新的套接字描述符,专门用于与这个客户端通信。
示例代码:
struct sockaddr_in cliaddr; socklen_t clilen = sizeof(cliaddr); int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen); if (connfd < 0) { perror("accept failed"); exit(EXIT_FAILURE); } printf("Connection from %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));2.5 客户端连接服务器
客户端使用connect()函数连接服务器:
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);示例代码:
struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8080); inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("connect failed"); exit(EXIT_FAILURE); }2.6 数据传输
连接建立后,双方可以使用read()/write()或send()/recv()函数进行数据传输:
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); #include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags);示例代码(服务器端接收数据):
char buffer[1024]; int n = read(connfd, buffer, sizeof(buffer)); if (n < 0) { perror("read error"); } else if (n == 0) { printf("Client disconnected\n"); } else { buffer[n] = '\0'; printf("Received: %s\n", buffer); // 回显数据 write(connfd, buffer, n); }2.7 关闭连接
通信完成后,使用close()函数关闭套接字:
#include <unistd.h> int close(int fd);示例代码:
close(connfd); close(sockfd);3. UDP套接字编程
UDP套接字编程比TCP简单,因为它不需要建立连接。下面介绍UDP套接字的基本使用方法。
3.1 创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); }3.2 绑定地址(服务器端)
struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(8080); if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); }3.3 发送数据(客户端)
使用sendto()函数发送数据:
#include <sys/types.h> #include <sys/socket.h> ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);示例代码:
struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8080); inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); char *message = "Hello UDP Server"; sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));3.4 接收数据(服务器端)
使用recvfrom()函数接收数据:
#include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);示例代码:
struct sockaddr_in cliaddr; socklen_t len = sizeof(cliaddr); char buffer[1024]; int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&cliaddr, &len); buffer[n] = '\0'; printf("Received: %s from %s:%d\n", buffer, inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));4. 高级话题与常见问题
4.1 非阻塞套接字
默认情况下,套接字操作是阻塞的。我们可以使用fcntl()函数将其设置为非阻塞模式:
#include <fcntl.h> int flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);在非阻塞模式下,如果操作不能立即完成,函数会返回-1并设置errno为EAGAIN或EWOULDBLOCK。
4.2 多路复用I/O
使用select()、poll()或epoll()可以同时监控多个套接字:
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);示例代码:
fd_set readfds; FD_ZERO(&readfds); FD_SET(sockfd, &readfds); struct timeval tv; tv.tv_sec = 5; tv.tv_usec = 0; int ret = select(sockfd + 1, &readfds, NULL, NULL, &tv); if (ret > 0) { if (FD_ISSET(sockfd, &readfds)) { // 套接字可读 } }4.3 常见错误处理
地址已在使用(Address already in use):
- 设置SO_REUSEADDR选项:
int opt = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
- 设置SO_REUSEADDR选项:
连接被拒绝(Connection refused):
- 检查服务器是否运行
- 检查防火墙设置
- 确认端口号是否正确
连接超时(Connection timed out):
- 检查网络连通性
- 增加超时时间
4.4 性能优化建议
- 使用大缓冲区减少系统调用次数
- 批量发送数据而不是频繁发送小数据包
- 对于高并发服务器,考虑使用线程池或事件驱动模型
- 合理设置TCP_NODELAY选项(禁用Nagle算法):
int flag = 1; setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
5. 实战案例:简单的TCP回显服务器
下面是一个完整的TCP回显服务器实现,它接收客户端发送的数据并将其原样返回:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8080 #define BUFFER_SIZE 1024 int main() { int sockfd, connfd; struct sockaddr_in servaddr, cliaddr; socklen_t clilen; char buffer[BUFFER_SIZE]; // 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 设置服务器地址 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(PORT); // 绑定套接字 if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 开始监听 if (listen(sockfd, 5) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } printf("Server listening on port %d...\n", PORT); while (1) { // 接受连接 clilen = sizeof(cliaddr); connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen); if (connfd < 0) { perror("accept failed"); continue; } printf("Connection from %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); // 处理连接 while (1) { int n = read(connfd, buffer, sizeof(buffer) - 1); if (n < 0) { perror("read error"); break; } else if (n == 0) { printf("Client disconnected\n"); break; } buffer[n] = '\0'; printf("Received: %s\n", buffer); // 回显数据 if (write(connfd, buffer, n) < 0) { perror("write error"); break; } } close(connfd); } close(sockfd); return 0; }对应的客户端实现:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERVER_IP "127.0.0.1" #define PORT 8080 #define BUFFER_SIZE 1024 int main() { int sockfd; struct sockaddr_in servaddr; char buffer[BUFFER_SIZE]; // 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 设置服务器地址 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(PORT); if (inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr) <= 0) { perror("invalid address"); exit(EXIT_FAILURE); } // 连接服务器 if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("connect failed"); exit(EXIT_FAILURE); } printf("Connected to server at %s:%d\n", SERVER_IP, PORT); while (1) { printf("Enter message (or 'quit' to exit): "); fgets(buffer, sizeof(buffer), stdin); // 移除换行符 buffer[strcspn(buffer, "\n")] = '\0'; if (strcmp(buffer, "quit") == 0) { break; } // 发送数据 if (write(sockfd, buffer, strlen(buffer)) < 0) { perror("write error"); break; } // 接收回显 int n = read(sockfd, buffer, sizeof(buffer) - 1); if (n < 0) { perror("read error"); break; } buffer[n] = '\0'; printf("Server reply: %s\n", buffer); } close(sockfd); return 0; }这个简单的回显服务器展示了TCP套接字编程的基本流程。在实际项目中,你可能需要添加更多的错误处理、日志记录和性能优化措施。