一、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/Administrator | 22(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--60999(Linux 默认临时范围) | 0--49151(知名 + 注册端口) |
1.1.4 理解socket
A. 核心概念
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 Short | 16 位主机序 -> 网络序 |
htonl() |
Host to Network Long | 32 位主机序 -> 网络序 |
ntohs() |
Network to Host Short | 16 位网络序 -> 主机序 |
ntohl() |
Network to Host Long | 32 位网络序 -> 主机序 |
命名记忆法 :
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 descriptor)
B. 参数详解
参数一:int domain(协议族/域)
核心功能:指定通信所使用的协议族,决定了 socket 的地址格式和底层协议栈。
常见值包括:
- AF_INET:IPv4 ★互联网协议
- AF_INET6:IPv6 互联网协议
- AF_UNIX / AF_LOCAL:★本地进程间通信
- AF_NETLINK:内核与用户空间之间的通信
- AF_PACKET:底层数据包接口(原始套接字)
参数二:int type(套接字类型)
核心功能:指定通信的语义和服务质量。
常见值包括:
- SOCK_STREAM:面向连接、可靠、有序的字节流(★对应 TCP)
- SOCK_DGRAM:无连接、不可靠、消息边界保留的数据报(★对应 UDP)
参数三:int protocol(具体协议)
核心功能:指定 domain 和 type 下使用的具体协议编号。
常见值:设为0
domain+type 组合下的默认协议:
AF_INET + SOCK_STREAM → 默认为 TCP (IPPROTO_TCP)
AF_INET + SOCK_DGRAM → 默认为 UDP (IPPROTO_UDP)
C. 返回值
成功:返回一个非负整数文件描述符
失败:返回 -1,并设置 errno
D. 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_in(IPv4)或 sockaddr_in6(IPv6)
参数三: 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,常见 errno:
E. 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_STREAM(TCP)有效。
B. 参数详解
参数一: socket
- 已 bind 的 TCP socket 文件描述符
参数二: backlog
- 内核为此 socket 排队的未完成连接队列最大长度。
- Linux 上实际受 /proc/sys/net/core/somaxconn 限制(默认 4096)
补充说明:backlog
backlog 过小:高并发时新连接被丢弃,客户端表现为超时或 RST。
backlog 过大:占用内核内存,但一般影响不大。
推荐值:根据并发量设定,常见 128、512、1024。
客户端 SYN ──→ [半连接队列 (SYN Queue)] ──→ [全连接队列 (Accept Queue)]
↑
backlog 限制的是这里
↓
accept() 从全连接队列取出
C. 返回值
- 成功:返回 0
- 失败:返回 -1
常见 errno:
EOPNOTSUPP:socket 类型不支持 listen(如 UDP)
EADDRNOTAVAIL:socket 未绑定地址
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 成功: 等待连接, backlog=128, fd=%d\n", sockfd);
printf("按 Ctrl+C 退出...\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 -字符串转换ip
"4字节网络风格的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 中接收数据。
- 仅适用于面向连接的 socket(TCP)
2.4.6 send -发送消息
1.头文件包含:#include <sys/socket.h>
2.函数原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
3.函数功能:
- 向已连接的 socket 发送数据。
- 仅适用于面向连接的 socket(TCP)