ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【Linux笔记】Socket编程基础

【Linux笔记】Socket编程基础 一、Socket编程基础1.1 端口号1.1.1 认识端口号端口号是一个 16 位无符号整数取值范围为 0 ~ 65535。它并非物理实体而是操作系统内核维护的一种软件抽象用于将接收到的数据包正确分发给对应的应用程序相应的进程。核心作用1.多路复用与解复用一台主机可同时运行多个网络应用Web 服务器、邮件服务、数据库等。传输层通过端口号区分这些应用实现单 IP 支撑多服务。简而言之区分同一台主机中的不同进程。2.服务标识约定俗成的端口号成为服务的名字。例如看到目标端口 80 即 HTTP看到目标端口 443 即 HTTPS。3.连接区分在 TCP 中一个四元组源IP、源端口、目的IP、目的端口唯一标识一条连接。同一服务器端口可同时服务成千上万客户端靠的就是不同的源端口组合。关键特性1.独立于 IP不同 IP 地址上的相同端口号互不干扰。如192.168.1.1:80与10.0.0.1:80是两个独立服务2.协议隔离TCP 端口 80 与 UDP 端口 80 是完全独立的命名空间可同时被不同程序绑定。3.临时性与持久性并存服务端端口通常长期固定客户端端口则动态分配、用完即释放。1.1.2 端口号的划分范围名称用途绑定权限典型示例0 – 1023知名端口系统级核心服务需 root/Administrator22(SSH), 53(DNS), 80(HTTP), 443(HTTPS)1024 – 49151注册端口用户级应用服务普通用户即可3306(MySQL), 5432(PostgreSQL), 8080(HTTP代理), 6379(Redis)49152 – 65535动态/私有端口客户端临时端口由内核自动分配浏览器、curl、API 调用等发起连接时使用端口 0保留不用作目的端口。但在bind()时指定端口 0表示请求内核自动分配一个可用的临时端口。1.1.3源端口号与目的端口号源端口号发送方本次通信使用的本地端口发送方内核在首次发送时自动分配除非应用显式bind,分配后仅在本次连接生命周期内有效。目的端口号接收方提供服务的监听端口接收方应用在服务启动时通过bind()显式指定或由协议标准约定。属性源端口号目的端口号含义发送方本次通信使用的本地端口接收方提供服务的监听端口分配方式通常由内核自动分配临时端口由服务端预先绑定知名/注册端口是否固定每次连接动态变化服务运行期间保持不变主要作用标识发送方供对方回送响应标识目标服务供接收方分用典型值域32768–60999Linux 默认临时范围0–49151知名 注册端口1.1.4 理解socketA. 核心概念Socket套接字是操作系统对网络通信端点的抽象它本质上是一个文件描述符File Descriptor。在 Linux/Unix 哲学中“一切皆文件”网络通信也被抽象为文件的读写操作。B. 双重身份通信端点代表了网络中的一个通信节点包含了 IP 地址和端口号的信息。文件描述符应用程序通过标准的 I/O 接口read/write/send/recv对其进行操作无需关心底层网络协议的复杂实现。C. Socket 与协议的关系Socket 本身不是协议而是访问传输层服务TCP/UDP的统一接口。创建 Socket 时指定的domain、type、protocol决定了该 Socket 遵循哪种网络协议规范。1.2 传输层的典型代表1.2.1 认识TCP协议TCP 的设计目标是提供可靠、有序、无差错的数据传输服务。它就像一次确认过的电话通话双方必须先建立连接沟通结束后再挂断。TCP的核心特性 ​ 1. 传输层协议 ​ 2. 有连接 ​ 3. 可靠传输 ​ 4. 面向字节流1.2.2 认识UDP协议UDP 的设计目标是提供简单、快速、低开销的数据传输服务。它就像寄明信片写完地址直接投出去不确认对方是否收到也不保证顺序。UDP的核心特性 ​ 1. 传输层协议 ​ 2. 无连接 ​ 3. 不可靠传输 ​ 4. 面向数据报1.3 网络字节序1.3.3 字节序的本质大端与小端字节序指的是多字节数据如 16 位、32 位整数在内存中的存放顺序。单字节数据不存在字节序问题。A. 大端字节序定义数据的高位字节Most Significant Byte, MSB存放在内存的低地址处低位字节Least Significant Byte, LSB存放在内存的高地址处。示例将 32 位整数0x12345678存入内存起始地址0x100地址: 0x100 0x101 0x102 0x103 内容: 0x12 0x34 0x56 0x78特点与人类书写和阅读数字的顺序一致从左到右先看到高位。网络协议如 TCP/IP通常采用大端字节序因此大端也被称为网络字节序。B. 小端字节序定义数据的低位字节Least Significant Byte, LSB存放在内存的低地址处高位字节Most Significant Byte, MSB存放在内存的高地址处。示例同样将0x12345678存入内存起始地址0x100地址: 0x100 0x101 0x102 0x103 内容: 0x78 0x56 0x34 0x12特点与人类书写顺序相反但在硬件实现上有优势低地址直接对应低位进行加减乘除或数据类型转换时不需要移动字节位置。主流处理器如 x86、大多数 ARM 默认配置采用小端字节序因此也叫主机字节序在 x86 平台上。在强制类型转换如int转char时直接取低地址即可得到低位字节效率较高。1.3.2 核心原则网络字节序 大端字节序1. 所有在网络上传输的多字节字段IP 地址、端口号、TCP/UDP 头部字段等必须以大端序排列。 ​ 2. 主机内部可使用任意字节序但在发送前转换为网络字节序接收后转换回主机字节序。1.3.3 字节序转换函数字节序转换函数相关的API接口#include arpa/inet.h ​ uint32_t htonl(uint32_t hostlong); ​ uint16_t htons(uint16_t hostshort); ​ uint32_t ntohl(uint32_t netlong); ​ uint16_t ntohs(uint16_t netshort);函数含义说明htons()Host to Network Short16 位主机序 - 网络序htonl()Host to Network Long32 位主机序 - 网络序ntohs()Network to Host Short16 位网络序 - 主机序ntohl()Network to Host Long32 位网络序 - 主机序命名记忆法h host,n network,s short(16bit),l long(32bit)二、 socket编程接口2.1 socket常见API// 创建 socket 文件描述符 (TCP/UDP, 客户端 服务器) int socket(int domain, int type, int protocol); ​ // 绑定端口号 (TCP/UDP, 服务器) int bind(int socket, const struct sockaddr *address, socklen_t address_len); ​ // 开始监听socket (TCP, 服务器) int listen(int socket, int backlog); ​ // 接收请求 (TCP, 服务器) int accept(int socket, struct sockaddr* address, socklen_t* address_len); ​ // 建立连接 (TCP, 客户端) int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);2.2 sockaddr 结构体系列struct sockaddr基类的结构体// sys/socket.h struct sockaddr { sa_family_t sa_family; // 地址族标识AF_INET, AF_INET6, AF_UNIX... char sa_data[14]; // 协议特定地址数据仅占位实际不使用 };struct sockaddr_in用于进程间网络通信// netinet/in.h typedef unsigned short int sa_family_t; typedef uint16_t in_port_t; // uint16_t 等价于 unsigned short int typedef uint32_t in_addr_t s_addr; // uint32_t 等价于 unsigned int struct sockaddr_in { sa_family_t sin_family; // ★ sin_family AF_INET ---用于网络通信 in_port_t sin_port; // ★ 端口号网络字节序 struct in_addr sin_addr; // ★ IPv4 地址网络字节序 unsigned char sin_zero[8]; // (无需特殊考虑只用于填充大小)填充至与 sockaddr 等长16字节 }; struct in_addr { in_addr_t s_addr; // 32位IPv4地址网络字节序 };struct sockaddr_un用于进程间本地通信// sys/un.h typedef unsigned short int sa_family_t; struct sockaddr_un { sa_family_t sun_family; // ★ sin_family AF_UNIX / AF_LOCAL ---用于本地通信 char sun_path[108]; // 文件系统路径以\0结尾 };2.3 socket核心接口详解2.3.1 socket -创建网络通信端点A. 函数概述1.头文件包含 #include sys/types.h #include sys/socket.h 2.函数原型 int socket(int domain, int type, int protocol); 3.函数功能 socket 用于创建一个网络通信端点endpoint返回一个文件描述符file descriptorB. 参数详解参数一int domain协议族/域 核心功能指定通信所使用的协议族决定了 socket 的地址格式和底层协议栈。 常见值包括 - AF_INETIPv4 ★互联网协议 - AF_INET6IPv6 互联网协议 - AF_UNIX / AF_LOCAL★本地进程间通信 - AF_NETLINK内核与用户空间之间的通信 - AF_PACKET底层数据包接口原始套接字参数二int type套接字类型 核心功能指定通信的语义和服务质量。 常见值包括 - SOCK_STREAM面向连接、可靠、有序的字节流★对应 TCP - SOCK_DGRAM无连接、不可靠、消息边界保留的数据报★对应 UDP参数三int protocol具体协议 核心功能指定 domain 和 type 下使用的具体协议编号。 常见值设为0 domaintype 组合下的默认协议 AF_INET SOCK_STREAM → 默认为 TCP (IPPROTO_TCP) AF_INET SOCK_DGRAM → 默认为 UDP (IPPROTO_UDP)C. 返回值成功返回一个非负整数文件描述符 失败返回 -1并设置 errnoD. Demo演示#include sys/types.h #include sys/socket.h #include stdio.h #include errno.h int main(void) { // 创建 IPv4 TCP socket int sockfd socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); if (sockfd -1) { perror(socket); return 1; } printf(Socket created, fd %d\n, sockfd); // 后续 bind/connect/send/recv ... close(sockfd); return 0; }2.3.2 bind -绑定ip与端口A.函数原型1.头文件包含 #include sys/socket.h 2.函数原型 int bind(int socket, const struct sockaddr *address, socklen_t address_len); 3.函数功能 将 socket 与本地地址IP 端口绑定。 - 服务器必须先 bind 才能接收连接 - 客户端通常无需手动 bind内核会自动分配临时端口。注一个进程可以绑定多个端口号但一个端口号不能被多个进程所绑定。B.参数详解参数一: int socket socket 返回的文件描述符参数二: const struct sockaddr *address 指向 sockaddr 结构体的指针实际传入 sockaddr_inIPv4或 sockaddr_in6IPv6参数三: socklen_t address_len 地址结构体的大小通常传 sizeof(struct sockaddr_in)C.关键结构体struct sockaddr_in { sa_family_t sin_family; ★/* AF_INET */ in_port_t sin_port; ★/* 端口号网络字节序 */ struct in_addr sin_addr; ★/* IP 地址 -通常设置为INADDR_ANY*/ char sin_zero[8]; /* 仅用于填充无特殊功能必须置零 */ };D. 返回值- 成功返回 0 - 失败返回 -1常见 errnoE. Demo演示#include stdio.h #include stdlib.h #include string.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include unistd.h int main(void) { int sockfd socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); if (sockfd -1) { perror(socket); return 1; } /* 设置 SO_REUSEADDR避免 TIME_WAIT 导致重启时 bind 失败 */ int opt 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(8080); /* 端口 8080转网络字节序 */ addr.sin_addr.s_addr htonl(INADDR_ANY); /* 监听所有网卡 */ if (bind(sockfd, (struct sockaddr *)addr, sizeof(addr)) -1) { perror(bind); close(sockfd); return 1; } printf(bind 成功: 0.0.0.0:8080, fd%d\n, sockfd); close(sockfd); return 0; }2.3.3 listen -开启监听A. 函数原型1.头文件声明: #include sys/socket.h 2.函数生声明: int listen(int socket, int backlog); 3.函数功能: - 将 socket 从主动状态转为被动监听状态使内核开始接受发往该 socket 的连接请求。 告诉内核我要开始接收连接了 - 内核开始处理 TCP 三次握手 - 维护连接队列 未完成连接队列收到 SYN还没完成三次握手。 已完成连接队列三次握手已经完成等待应用程序 accept - 仅对 SOCK_STREAMTCP有效。B. 参数详解参数一: socket - 已 bind 的 TCP socket 文件描述符 参数二: backlog - 内核为此 socket 排队的未完成连接队列最大长度。 - Linux 上实际受 /proc/sys/net/core/somaxconn 限制默认 4096补充说明backlogbacklog 过小高并发时新连接被丢弃客户端表现为超时或 RST。backlog 过大占用内核内存但一般影响不大。推荐值根据并发量设定常见 128、512、1024。客户端 SYN ──→ [半连接队列 (SYN Queue)] ──→ [全连接队列 (Accept Queue)] ↑ backlog 限制的是这里 ↓ accept() 从全连接队列取出C. 返回值- 成功返回 0 - 失败返回 -1 常见 errno EOPNOTSUPPsocket 类型不支持 listen如 UDP EADDRNOTAVAILsocket 未绑定地址D. Demo演示#include stdio.h #include stdlib.h #include string.h #include sys/socket.h #include netinet/in.h #include unistd.h int main(void) { int sockfd socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); if (sockfd -1) { perror(socket); return 1; } int opt 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(9090); addr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(sockfd, (struct sockaddr *)addr, sizeof(addr)) -1) { perror(bind); close(sockfd); return 1; } if (listen(sockfd, 128) -1) { perror(listen); close(sockfd); return 1; } printf(listen 成功: 等待连接, backlog128, fd%d\n, sockfd); printf(按 CtrlC 退出...\n); pause(); /* 挂起模拟服务器等待状态 */ close(sockfd); return 0; }2.3.4 accept -接受连接 (服务端)A. 函数原型1. #include sys/socket.h 2. 函数声明: int accept(int socket, struct sockaddr *address, socklen_t *address_len); 3. 函数功能: - 从全连接队列中取出一个已完成的连接创建一个新的 socket 文件描述符用于与该客户端通信。 - 原始监听 socket 继续接受后续连接。B. 参数详解参数一: socket - 处于 LISTEN 状态的 socket 文件描述符 参数二: struct sockaddr *address - 输出参数内核填入客户端的地址信息IP 端口。若不需要可传 NULL 参数三: socklen_t *address_len - 输入/输出参数。调用前填入缓冲区大小返回时填入实际写入的长度C. 返回值- 成功返回新的文件描述符非负整数代表与客户端的连接 - 失败返回 -1 常见 errno EAGAIN / EWOULDBLOCK非阻塞模式下队列为空 ECONNABORTED连接在队列中已被中止如客户端发 RST EINTR被信号中断 EMFILE文件描述符耗尽D. Demo演示#include stdio.h #include stdlib.h #include string.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include unistd.h int main(void) { int listenfd socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); if (listenfd -1) { perror(socket); return 1; } int opt 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(9090); addr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(listenfd, (struct sockaddr *)addr, sizeof(addr)) -1) { perror(bind); close(listenfd); return 1; } if (listen(listenfd, 128) -1) { perror(listen); close(listenfd); return 1; } printf(服务器已就绪等待客户端连接...\n); struct sockaddr_in cliaddr; socklen_t cliaddrlen sizeof(cliaddr); /* 阻塞等待客户端连接 */ int connfd accept(listenfd, (struct sockaddr *)cliaddr, cliaddrlen); if (connfd -1) { perror(accept); close(listenfd); return 1; } char ip_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, cliaddr.sin_addr, ip_str, sizeof(ip_str)); printf(accept 成功: 客户端 %s:%d, connfd%d\n, ip_str, ntohs(cliaddr.sin_port), connfd); close(connfd); close(listenfd); return 0; }2.3.5 connect -发起连接 (客户端)A. 函数原型1. 头文件包含#include sys/socket.h 2. 函数原型int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 3. 函数功能 - 客户端调用此函数向服务器发起 TCP 三次握手。 - 成功后 socket 进入 ESTABLISHED 状态可直接 send()/recv()。B. 参数详解参数一: sockfd - socket() 返回的文件描述符。 参数二: addr - 指向目标服务器地址结构体的指针通常为 struct sockaddr_in。 参数三: addrlen - 地址结构体的大小通常为 sizeof(struct sockaddr_in)。C. 返回值成功返回 0 失败返回 -1 常见 errno ECONNREFUSED目标端口未监听或服务器拒绝连接 ETIMEDOUT连接超时网络不可达或防火墙丢包 EINPROGRESS非阻塞模式下连接正在进行中 ENETUNREACH网络不可达D. Demo演示#include stdio.h #include stdlib.h #include string.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include unistd.h int main(void) { int sockfd socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); if (sockfd -1) { perror(socket); return 1; } struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_port htons(9090); inet_pton(AF_INET, 127.0.0.1, server_addr.sin_addr); if (connect(sockfd, (struct sockaddr *)server_addr, sizeof(server_addr)) -1) { perror(connect); close(sockfd); return 1; } printf(connect 成功: 已连接到 127.0.0.1:9090\n); // 后续可进行 send/recv 操作... close(sockfd); return 0; }2.4 socket其他函数接口2.4.1 inet_pton -ip转换字符串点分十进制的字符串 转换为 4字节网络风格的IP#include arpa/inet.h int inet_pton(int af, const char *src, void *dst); 参数一 af: AF_INET / AF_INET6 参数二 src: 源字符串 参数三 dst: 输出x,一般传入 struct in_addr 类型2.4.2 inet_ntop -字符串转换ip4字节网络风格的IP 转换为 点分十进制的字符串#include arpa/inet.h const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);2.4.3 recvfrom -接受消息#include sys/socket.h ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);2.4.4 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);2.4.5 recv -接受消息1.头文件包含#include sys/socket.h 2.函数原型ssize_t recv(int sockfd, void *buf, size_t len, int flags); 3.函数功能 - 从已连接的 socket 中接收数据。 - 仅适用于面向连接的 socketTCP2.4.6 send -发送消息1.头文件包含#include sys/socket.h 2.函数原型ssize_t send(int sockfd, const void *buf, size_t len, int flags); 3.函数功能 - 向已连接的 socket 发送数据。 - 仅适用于面向连接的 socketTCP
返回列表