网络基础与套接字编程详解
摘要本文系统介绍了网络编程的基础知识涵盖IP地址、端口号、网络通信协议、网络字节序、TCP/IP五层模型等核心概念并详细讲解了套接字编程的原理与实践。文章从网络通信的基本要素出发逐步深入到协议分层、数据封装解封装过程最后重点阐述了Socket API接口的使用方法包括创建套接字、绑定地址信息、连接服务器等关键操作为读者构建了完整的网络编程知识体系。1. IP地址本质IPv4版本4字节无符号整数IPv6版本16字节的数据作用在网络中唯一标识一台主机。IP地址不够用的解决方案DHCP动态主机配置协议动态地址分配NAT/NAPT网络地址转换/网络地址端口转换通过地址转换技术复用IP核心要素网络通信中的每条数据中都会包含源端IP和对端IP。2. 端口号本质2字节无符号整数。作用在一台主机上唯一标识一个网络通信程序。网络通信程序通过端口号告诉操作系统网卡收到了发往哪个端口的数据应该交给我来进行处理。核心要素网络通信中的每条数据中都会包含源端口和对端端口。重要规则一个进程可以占用多个端口号但是一个端口号只能被一个进程占用。3. 网络通信协议本质网络通信中通信两端的数据格式约定。基于网络通信环境的复杂性对网络通信协议进行分层不同的层次使用不同的协议解决不同的问题。OSI七层模型应用层、表示层、会话层、传输层、网络层、链路层、物理层。TCP/IP五层模型应用层、传输层、网络层、链路层、物理层。数据封装与解封装过程发送端自上而下封装应用层你好 → qq 你好传输层tcp qq 你好网络层ip tcp qq 你好链路层eth ip tcp qq 你好经过网络传输接收端自下而上解封装链路层剥掉eth头部 → ip tcp qq 你好网络层剥掉ip头部 → tcp qq 你好传输层剥掉tcp头部 → qq 你好应用层得到“你好”网络通信层次中的设备物理层集线器等链路层交换机负责相邻设备的选择与转发网络层路由器负责起点到终点的路径选择传输层PC主机五元组源端IP 源端端口 对端IP 对端端口 通信协议。五元组完整地描述了一条网络通信。4. 网络字节序字节序CPU在内存中进行数据存取的顺序。二进制存在高位和低位之分左边是高位右边是低位例如 0x0102。内存存储存在高地址和低地址之分一块空间起始处总是低地址。一个存储单元大于1字节的数据就会存在字节序的问题。字节序类型大端字节序低地址存高位。例如 0x01020304 → 内存依次为 01 02 03 04。小端字节序低地址存低位。例如 0x01020304 → 内存依次为 04 03 02 01。字节序的决定性因素CPU架构例如 X86 为小端MIPS 为大端。字节序对网络通信的影响非常重要。如果通信双方字节序不一致直接传输原始数据会导致解析错误。网络字节序实际上就是网络通信中数据的字节序标准。它约定在网络通信中所有数据都应该转换为大端字节序后进行传输。有了这个约定数据接收方就可以根据自己的主机字节序决定如何对收到的数据进行字节序转换。5. TCP/IP 五层模型各层功能与协议应用层负责应用程序之间的数据沟通。例如QQ与QQ程序之间沟通有自己的协议数据格式约定约定源QQ号、对端QQ号、时间、字体、消息类型等。应用层在进行通信时先要对数据进行描述要素多了就需要约定如何组织。典型协议HTTP、FTP、DNS。传输层负责两个进程之间的数据传输。传输层约定的关键信息源端口号 对端端口号。典型协议TCP、UDP。TCP可靠传输协议安全但传输性能略低于UDP。UDP不可靠传输协议不安全但传输性能高。网络层负责两个主机之间的数据传输。网络层约定的关键信息源端IP 对端IP。典型协议IP协议。链路层负责相邻设备之间的数据传输。链路层约定的关键信息源端MAC地址 对端MAC地址。MAC地址网卡的物理硬件地址出厂时设定。典型协议以太网协议。6. 套接字编程概述套接字编程做什么编写网络通信程序实现两个主机上的进程之间的数据传输。网络通信也是进程间通信IPC方式的一种相较于以前的进程间通信它可以跨主机。通信两端的角色客户端通常指的是部署在客户机上发起业务请求的一端。服务端通常指的是部署在服务提供商的服务器上向客户端提供服务的一端。通信架构C/S客户端/服务器模式程序员既要实现服务端程序也要实现客户端程序。谁安装了客户端谁才能与服务器通信。B/S浏览器/服务器模式程序员通常只需要实现服务端程序客户端不需要实现客户端就是浏览器。我们学习的网络编程称为套接字编程Socket 编程。学习内容基于 Socket 套接字相关的 API 接口实现网络通信的客户端与服务端程序。Socket 套接字一套网络通信 API 接口。7. 通信程序编写流程与 Socket API主要关心的是传输层通信程序的编写。传输层有两个典型协议TCP 和 UDP。7.1 Socket API 接口1. 创建套接字int socket(int domain, int type, int protocol);domain地址域描述通信地址类型。preulAF_UNIX域间通信本地通信使用struct sockaddr_un。AF_INETIPv4 通信使用struct sockaddr_in。AF_INET6IPv6 通信使用struct sockaddr_in6。type套接字类型SOCK_STREAM流式套接字有序的、可靠的、双向的基于字节流对应TCP。SOCK_DGRAM数据报套接字无连接的、不可靠的有最大长度限制对应UDP。SOCK_RAW提供更底层原始的数据包捕获。protocol协议类型选择要使用的协议默认值 0。SOCK_STREAM默认 TCP。SOCK_DGRAM默认 UDP。也可显式指定IPPROTO_TCPTCP协议、IPPROTO_UDPUDP协议。返回值成功则返回操作句柄套接字描述符失败返回 -1。2. 为套接字绑定地址信息int bind(int sockfd, struct sockaddr* addr, socklen_t len);作用设置发送数据时的源端地址信息当前设备网卡IP地址端口号告知操作系统网卡收到的哪些数据应该交给当前套接字进行处理参数sockfd套接字描述符socket()返回的描述符。addr要绑定的地址信息可用结构体struct sockaddr_un、struct sockaddr_in6、struct sockaddr_in。len地址信息长度。地址结构体示例IPv4struct sockaddr_in { ushort sin_family, // 地址域类型填充 AF_INET ushort sin_port, // 要绑定的端口号 struct in_addr { uint32 s_addr; } sin_addr; // 要绑定的 IP 地址 };端口号规则0~1024 是预留端口需要管理员权限返回值成功返回 0失败返回 -1。3. 连接服务器用于 TCP 协议客户端int connect(int sockfd, struct sockaddr* addr, socklen_t len);参数sockfd套接字描述符。addr要连接的服务器地址信息。len地址信息长度。返回值成功返回 0失败返回 -1。4. 开始监听用于 TCP 协议的服务端int listen(int sockfd, int backlog);目的告诉操作系统可以开始接受客户端的连接请求了。backlog同一时间能够接受最大客户端连接请求数量。限制同一时间最多有多少个客户端能够连接服务器。返回值成功返回 0失败返回 -1。5. 获取就绪连接用于 TCP 协议的服务端int accept(int sockfd, struct sockaddr* peer_addr, socklen_t* len);作用获取与指定客户端进行通信的操作句柄描述符。peer_addr获取对端地址信息。len既要设置要获取多长的地址并且还要返回实际的地址信息长度。函数的参数有三种分类输入型参数输出型参数输入输出型参数。返回值成功则返回指定客户端通信句柄描述符失败返回 -1。6. 接收数据接收数据的接口是一个阻塞接口recv 的时候没有数据则等待。TCP 接收ssize_t recv(int sockfd, void* buf, size_t len, int flag);sockfd对于客户端来说就是 socket 的返回值对于服务端来说是 accept 的返回值。flagMSG_DONTWAIT本次 recv 能拿到数据则直接返回没有数据则报错返回。MSG_PEEK接收数据但是不从缓冲区删除数据。返回值成功则返回实际读取到的数据长度。出错则返回 -1。TCP 通信连接断开返回 0。UDP 接收接口ssize_t recvfrom(int sockfd, void* buf, size_t len, int flag, struct sockaddr* peer_addr, socklen_t* addrlen);peer_addr输出参数返回对端的地址信息表示该数据是谁发过来的。7. 发送数据发送数据相关的接口也是阻塞接口发送过快数据将 socket 发送缓冲区塞满了就会阻塞。ssize_t send(int sockfd, char* data, size_t len, int flag);ssize_t sendto(int sockfd, char* data, size_t len, int flag, struct sockaddr* toaddr, socklen_t addrlen);toaddr描述数据要发送给谁。返回值实际发送的数据长度出错则返回 -1。8. 关闭套接字int close(int fd);9. 字节序转换接口字节序转换针对的数据类型short, int, long, float, double, 无符号。主机字节序 → 网络字节序2 字节数据转换uint16_t htons(uint16_t val);4 字节数据转换uint32_t htonl(uint32_t val);注意短整型数据的转换不能使用 htonl。网络字节序 → 主机字节序uint16_t ntohs(uint16_t val);uint32_t ntohl(uint32_t val);10. IP 地址相关转换接口IP 本质4 字节无符号整数可以用 ntohlhtonl 进行转换。日常使用点分十进制字符串表示 IP因此需要专门接口struct in_addr inet_addr(const char *ipstr);将字符串点分十进制 IP 地址直接转换为网络字节序数字 IP 地址。const char* inet_ntoa(struct in_addr addr);将网络字节序数字 IP 地址转换为字符串点分十进制 IP 地址。注意inet_ntoa 接口是非线程安全的。const char* inet_ntop(int domain, void* src, char* dst, size_t size);功能将网络字节序整数 IP 地址转换为字符串 IP 地址。domainAF_INET、AF_INET6描述要转换的地址类型。src网络字节序的数据地址。dst转换后的字符串存放空间地址。sizedst 空间的大小。int inet_pton(int domain, const char *src, void *dst);功能将字符串的 IP 地址转换为网络字节序 IP 地址。11. UDP 通信程序编写流程通常客户端程序中写死了服务器 IP 地址客户端必然知道服务端的地址。UDP 客户端流程创建套接字socket()绑定地址信息通常客户端不主动绑定地址操作系统检测套接字未绑定地址时自动从闲置端口池分配端口 IP 完成绑定发送数据sendto()发送数据自带完整五元组源端地址由 bind 确定对端地址由 sendto 传入接收响应recvfrom()关闭套接字close()UDP 服务端流程创建套接字socket()为套接字绑定地址bind()接收数据recvfrom()向客户端响应数据sendto()关闭套接字close()7.2 套接字编程本质与TCP/IP四层模型套接字编程本质学习套接字 socket API 相关的网络编程接口。网络通信程序的角色之分客户端主动向服务器发送请求获取服务的一端通常部署在客户机上。服务端通常部署在网络服务提供商的服务器上接收客户端请求提供服务。TCP/IP四层模型应用层负责应用程序的沟通典型知名协议HTTP、DNS。传输层负责进程之间的数据传输典型知名协议TCP、UDP运行于PC机。网络层负责主机之间的数据传输典型知名协议IP运行于路由器。链路层负责相邻设备之间的数据传输典型知名协议ETH、ARP协议。8. TCP 多进程服务器实战示例下面是一个完整的 C TCP 多进程服务器示例演示了如何使用 Socket API 创建一个支持多客户端连接的简单字典翻译服务器。#include stdio.h #include string.h #include stdlib.h #include unistd.h // close #include sys/socket.h // socket api #include netinet/in.h // struct sockaddr_in #include arpa/inet.h // 字节序转换 htons #include signal.h #include string #include unordered_map // hash Map--键值对 #define CHECK_RESULT(ret, msg) if((ret) 0) { perror(msg); return -1; } std::unordered_mapstd::string,std::string dict { {hello, 你好}, {leihou, 你好}, {eat, 吃饭} }; // 实现一个多进程版本的服务器: 创建进程在子进程中与客户端进行通信 void worker(int sockfd) { pid_t pid fork(); if (pid 0) { perror(创建子进程失败); return ; } if (pid 0) { return; } while(1) { char buffer[1024] {0}; int ret recv(sockfd, buffer, 1023, 0); if (ret lt; 0) { perror(接收数据出错); continue; }else if (ret 0) { printf(与客户端连接断开\n); break; } printf(客户端say: %s\n, buffer); std::string resp; auto it dict.find(buffer); if (it dict.end()) { resp 当前字典中没有该内容; }else { resp it-amp;gt;second; } ret send(sockfd, resp.c_str(), resp.size(), 0); if (ret ! resp.size()) { if (ret amp;lt; 0) { perror(发送数据出错); close(sockfd); continue; } printf(数据发送不完整\n); } } close(sockfd); exit(0); // 当前在子进程中这里退出的是子进程 } int main(){ // 主动忽略 SIGCHLD 信号--告诉系统我不关注子进程状态 // 显式的告诉系统子进程退出后直接释放所有资源 signal(SIGCHLD, SIG_IGN); // 1. 创建套接字 // int socket(int domain, int type, int protocol) int listen_fd socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); CHECK_RESULT(listen_fd, 创建套接字失败); // 2. 构造监听地址结构对象为套接字绑定地址信息 // int bind(int fd, struct sockaddr* addr, socklen_t len) struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(9001); addr.sin_addr.s_addr inet_addr(192.168.14.128); socklen_t addrlen sizeof(struct sockaddr_in); int ret bind(listen_fd, (struct sockaddr*)amp;addr, addrlen); CHECK_RESULT(ret, 绑定地址失败); // 3. 开始监听 // int listen(int sockfd, int backlog) ret listen(listen_fd, 1024); CHECK_RESULT(ret, 监听失败); while(1){ struct sockaddr_in peerAddr; int newfd accept(listen_fd, (struct sockaddr*)amp;peerAddr, amp;addrlen); if (newfd lt; 0) { perror(获取新连接出错); continue; } char peer_ip[24] {0}; inet_ntop(AF_INET, amp;amp;peerAddr.sin_addr, peer_ip, 23); printf(新的客户端连接建立成功: [%s:%d]\n, peer_ip, ntohs(peerAddr.sin_port)); worker(newfd); close(newfd); // 父进程中并没有用到这个描述符 } return 0; }8.1 代码解析1. 头文件说明stdio.h、string.h、stdlib.h标准 C 库函数unistd.h提供 close、fork 等系统调用sys/socket.hSocket API 相关函数和数据结构netinet/in.h网络地址结构定义arpa/inet.h字节序转换函数htons、inet_addr 等signal.h信号处理相关string、unordered_mapC 标准库容器2. 核心功能模块字典数据结构std::unordered_mapstd::string,std::string dict { {hello, 你好}, {leihou, 你好}, {eat, 吃饭} };使用哈希表存储简单的英汉翻译字典客户端发送英文单词服务器返回对应的中文翻译。3. 多进程处理函数 worker()通过fork()创建子进程处理客户端连接父进程立即返回继续监听新连接子进程进入循环接收客户端数据并响应使用recv()接收数据send()发送响应连接断开时子进程退出4. 主函数 main() 流程信号处理忽略 SIGCHLD 信号避免僵尸进程创建套接字使用socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)创建 TCP 套接字绑定地址构造sockaddr_in结构绑定到 192.168.14.128:9001开始监听调用listen()设置最大连接数为 1024接受连接循环调用accept()接受客户端连接多进程处理为每个连接创建子进程处理8.2 关键 Socket API 使用1. socket() 创建套接字int listen_fd socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);AF_INETIPv4 地址族SOCK_STREAM流式套接字TCPIPPROTO_TCPTCP 协议2. bind() 绑定地址struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(9001); addr.sin_addr.s_addr inet_addr(192.168.14.128);htons()将主机字节序的端口号转换为网络字节序inet_addr()将点分十进制 IP 字符串转换为网络字节序整数3. listen() 开始监听ret listen(listen_fd, 1024);设置最大连接队列长度为 1024超过此数量的连接请求将被拒绝。4. accept() 接受连接int newfd accept(listen_fd, (struct sockaddr*)peerAddr, addrlen);返回新的套接字描述符用于与客户端通信peerAddr保存客户端地址信息inet_ntop()将网络字节序 IP 转换为可读字符串5. 多进程模型主进程负责接受连接accept()为每个连接创建子进程fork()子进程处理具体业务逻辑recv()/send()使用signal(SIGCHLD, SIG_IGN)避免僵尸进程8.3 编译与运行编译命令g -stdc11 -o tcp_server tcp_server.cpp运行服务器./tcp_server客户端测试使用 telnet 或 nctelnet 192.168.14.128 9001 # 或 nc 192.168.14.128 9001连接成功后输入 hello、leihou 或 eat 等字典中的单词服务器会返回对应的中文翻译。8.4 注意事项实际部署时需要将 IP 地址改为服务器的实际 IP多进程模型适合连接数不多的场景连接数多时建议使用多线程或 I/O 多路复用代码中使用了简单的错误处理实际生产环境需要更完善的错误处理机制字典数据可以扩展为从文件或数据库加载注意资源管理确保所有套接字在适当的时候关闭9. TCP 多线程服务器实战示例下面是一个完整的 C TCP 多线程服务器示例演示了如何使用 Socket API 和 pthread 库创建一个支持多客户端连接的简单聊天服务器。#include stdio.h #include string.h #include stdlib.h #include unistd.h // close #include sys/socket.h // socket api #include netinet/in.h // struct sockaddr_in #include arpa/inet.h // 字节序转换 htons #include pthread.h #define CHECK_RESULT(ret, msg) if((ret) 0) { perror(msg); return -1; } void* entry(void *arg) { int sockfd (int)arg; while(1) { char buffer[1024] {0}; int ret recv(sockfd, buffer, 1023, 0); if (ret 0) { perror(接收数据出错); continue; }else if (ret 0) { printf(与客户端连接断开\n); break; } printf(客户端say: %s\n, buffer); // 捕获键盘输入向客户端发送数据 memset(buffer, 0x00, 1024); printf(输入数据:); fflush(stdout); scanf(%s, buffer); ret send(sockfd, buffer, strlen(buffer), 0); if (ret ! strlen(buffer)) { if (ret amp;lt; 0) { perror(发送数据出错); close(sockfd); continue; } printf(数据发送不完整\n); } } close(sockfd); } // 创建一个线程负责通过当前的描述符与客户端进行通信 void worker(int sockfd) { pthread_t tid; int ret pthread_create(tid, NULL, entry, (void*)sockfd); if (ret ! 0) { perror(创建工作线程失败); return; } pthread_detach(tid); // 分离线程让线程退出后自动释放所有资源不需要被等待 } int main(){ // 1. 创建套接字 // int socket(int domain, int type, int protocol) int listen_fd socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); CHECK_RESULT(listen_fd, 创建套接字失败); // 2. 构造监听地址结构对象为套接字绑定地址信息 // int bind(int fd,struct sockaddr* addr, socklen_t len) struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(9001); addr.sin_addr.s_addr inet_addr(192.168.14.128); socklen_t addrlen sizeof(struct sockaddr_in); int ret bind(listen_fd, (struct sockaddr*)amp;addr, addrlen); CHECK_RESULT(ret, 绑定地址失败); // 3. 开始监听 // int listen(int sockfd, int backlog) ret listen(listen_fd, 1024); CHECK_RESULT(ret, 监听失败); while(1){ struct sockaddr_in peerAddr; int newfd accept(listen_fd, (struct sockaddr*)amp;peerAddr, amp;addrlen); if (newfd lt; 0) { perror(获取新链接出错); continue; } char peer_ip[24] {0}; inet_ntop(AF_INET, amp;amp;peerAddr.sin_addr, peer_ip, 23); printf(新的客户端连接建立成功: [%s:%d]\n,peer_ip, ntohs(peerAddr.sin_port)); worker(newfd); // close(newfd); 线程间资源共享这里关闭会导致工作线程无法使用 } return 0; }9.1 代码解析1. 头文件说明stdio.h、string.h、stdlib.h标准 C 库函数unistd.h提供 close 等系统调用sys/socket.hSocket API 相关函数和数据结构netinet/in.h网络地址结构定义arpa/inet.h字节序转换函数htons、inet_addr、inet_ntop 等pthread.hPOSIX 线程库用于多线程编程2. 线程处理函数 entry()参数接收客户端套接字描述符作为参数功能循环接收客户端数据并响应接收数据使用recv()接收客户端发送的数据发送数据从标准输入读取用户输入通过send()发送给客户端连接管理检测连接断开recv()返回 0时退出循环3. 线程创建函数 worker()使用pthread_create()创建新线程处理客户端连接线程分离调用pthread_detach()分离线程线程退出后自动释放资源线程参数将客户端套接字描述符传递给线程处理函数4. 主函数 main() 流程创建套接字使用socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)创建 TCP 套接字绑定地址构造sockaddr_in结构绑定到 192.168.14.128:9001开始监听调用listen()设置最大连接队列长度为 1024接受连接循环调用accept()接受客户端连接多线程处理为每个连接创建独立线程处理9.2 关键 Socket API 使用1. socket() 创建套接字int listen_fd socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);AF_INETIPv4 地址族SOCK_STREAM流式套接字TCPIPPROTO_TCPTCP 协议2. bind() 绑定地址struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(9001); addr.sin_addr.s_addr inet_addr(192.168.14.128);htons()将主机字节序的端口号转换为网络字节序inet_addr()将点分十进制 IP 字符串转换为网络字节序整数3. listen() 开始监听ret listen(listen_fd, 1024);设置最大连接队列长度为 1024超过此数量的连接请求将被拒绝。4. accept() 接受连接int newfd accept(listen_fd, (struct sockaddr*)peerAddr, addrlen);返回新的套接字描述符用于与客户端通信peerAddr保存客户端地址信息inet_ntop()将网络字节序 IP 转换为可读字符串5. 多线程模型主线程负责接受连接accept()为每个连接创建工作线程pthread_create()工作线程处理具体业务逻辑recv()/send()使用pthread_detach()避免需要等待线程结束9.3 编译与运行编译命令gcc -o tcp_thread_server tcp_thread_server.c -lpthread运行服务器./tcp_thread_server客户端测试使用 telnet 或 nctelnet 192.168.14.128 9001 # 或 nc 192.168.14.128 9001连接成功后服务器会显示客户端发送的消息并等待用户输入回复消息。9.4 注意事项实际部署时需要将 IP 地址改为服务器的实际 IP多线程模型相比多进程模型资源开销更小适合高并发场景线程间共享进程资源需要注意线程安全问题代码中使用了简单的错误处理实际生产环境需要更完善的错误处理机制注意资源管理确保所有套接字在适当的时候关闭编译时需要链接 pthread 库-lpthread线程分离后不需要调用pthread_join()等待线程结束9.5 与多进程模型的对比资源开销线程比进程更轻量创建和销毁开销更小资源共享线程共享进程的地址空间进程有独立的地址空间通信方式线程间通信更简单共享内存进程间通信需要 IPC 机制稳定性一个线程崩溃可能影响整个进程进程间相互隔离更安全适用场景多线程适合 I/O 密集型任务多进程适合 CPU 密集型任务