news 2026/11/2 14:43:25

Linux网络编程:TCP/UDP套接字详解与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux网络编程:TCP/UDP套接字详解与实践

1. 套接字基础概念与网络通信模型

套接字(Socket)是Linux网络编程中最核心的概念之一,它本质上是一个通信端点,可以理解为网络数据传输的"插座"。在实际开发中,我们通过创建套接字来建立网络连接、发送和接收数据。

1.1 套接字的基本组成

一个完整的套接字由以下几个关键要素组成:

  • IP地址:标识网络中的主机(如192.168.1.1)
  • 传输协议:TCP或UDP
  • 端口号:标识主机上的具体服务(如80端口对应HTTP服务)

这三者的组合可以表示为:套接字 = IP地址 + 传输协议 + 端口号。在网络通信中,套接字总是成对出现的——服务器端套接字和客户端套接字。

1.2 套接字通信的基本流程

典型的套接字通信遵循以下步骤:

  1. 服务器端:

    • 创建套接字(socket())
    • 绑定IP和端口(bind())
    • 开始监听(listen())
    • 接受连接(accept())
    • 收发数据(read()/write())
    • 关闭连接(close())
  2. 客户端:

    • 创建套接字(socket())
    • 连接服务器(connect())
    • 收发数据(read()/write())
    • 关闭连接(close())

这个流程是TCP套接字通信的基础模型,也是我们后续要详细讲解的核心内容。

1.3 套接字的类型

Linux系统中主要支持以下几种套接字类型:

  1. 流式套接字(SOCK_STREAM):

    • 基于TCP协议
    • 提供可靠的、面向连接的字节流服务
    • 保证数据按序到达,无差错
  2. 数据报套接字(SOCK_DGRAM):

    • 基于UDP协议
    • 提供无连接的数据报服务
    • 不保证可靠传输,但效率更高
  3. 原始套接字(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 常见错误处理

  1. 地址已在使用(Address already in use):

    • 设置SO_REUSEADDR选项:
      int opt = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  2. 连接被拒绝(Connection refused):

    • 检查服务器是否运行
    • 检查防火墙设置
    • 确认端口号是否正确
  3. 连接超时(Connection timed out):

    • 检查网络连通性
    • 增加超时时间

4.4 性能优化建议

  1. 使用大缓冲区减少系统调用次数
  2. 批量发送数据而不是频繁发送小数据包
  3. 对于高并发服务器,考虑使用线程池或事件驱动模型
  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套接字编程的基本流程。在实际项目中,你可能需要添加更多的错误处理、日志记录和性能优化措施。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 14:43:17

基于SDN的园区网络检测系统:若依工程部署与二次开发

简介&#xff1a;基于SDN的园区网络检测系统是一套前后端分离的完整项目源码&#xff0c;面向网络工程、计算机及相关专业学生、老师与研发人员&#xff0c;尤其适合作为毕业设计、课程设计或SDN方向入门进阶的参考实现。项目以Java为后端主体&#xff0c;包含264个Java文件&am…

作者头像 李华
网站建设 2026/11/2 14:41:44

PDF转图片中文空白:PDFBox字体替换与CMap排查

做PDF转图片这个需求&#xff0c;几乎每个后端团队都会碰到一次&#xff1a;合同、发票、报告、报表&#xff0c;业务方一句"要图片&#xff0c;别给PDF"&#xff0c;剩下的活就落在我们身上。用PDFBox做这件事本身不难&#xff0c;PDFRenderer几行代码就能出图&…

作者头像 李华
网站建设 2026/11/2 14:40:07

英特尔CPU+GPU运行Whisper实战指南:量化、加速与工程落地

1. 为什么非得在英特尔平台跑Whisper&#xff1f;——从“能跑”到“跑得稳、跑得快”的真实动因最近有位做教育类AI工具的某开发者朋友&#xff0c;发来一段语音转文字的测试片段&#xff0c;用的是某云服务API&#xff0c;识别结果里“量子力学”被写成“凉子力学”&#xff…

作者头像 李华
网站建设 2026/11/2 14:39:46

三大云服务商市场格局与核心技术对比分析

1. 三大云服务商市场格局解析 根据最新市场调研数据&#xff0c;全球云计算市场正在经历新一轮洗牌。阿里云以46.4%的市场份额持续领跑&#xff0c;这个数字背后是其在亚太地区特别是中国市场的绝对优势。腾讯云以18%的占比位居第二&#xff0c;其强项在于社交、游戏等垂直领域…

作者头像 李华
网站建设 2026/11/2 14:35:54

一枚回形针的隐藏宇宙:从机械设计到发散思维与AI安全

1. 一枚回形针&#xff0c;为什么值得专门写一篇长文 办公桌上总会躺着一些存在感极低的物件&#xff0c;回形针绝对排得上头号。一盒回形针&#xff08;paperclip&#xff09;买回来可能三五年都用不完&#xff0c;但真把这圈铁丝玩明白的人会告诉你&#xff1a;它是全世界最被…

作者头像 李华
网站建设 2026/11/2 14:35:31

企业订货管理系统(OMS)选型与实施全指南

1. 数字经济时代企业订货管理系统的核心价值 在数字化转型浪潮中&#xff0c;订货管理系统&#xff08;OMS&#xff09;已成为企业供应链管理的神经中枢。根据Gartner最新调研&#xff0c;采用专业OMS的企业平均降低23%的库存成本&#xff0c;订单处理效率提升40%以上。一套优秀…

作者头像 李华