1 ~> 网络通信的本质
网络通信的本质就是两个主机的进程之间在通信,网络就是两个进程看到的同一份资源。即两个进程通过网络进行数据交互:从远端获取数据和将数据发送到远端。
那么要能够通信就需要:
- 主机A要能够找到主机B;
- 进程A要能够找到进程B。
2~> 理解源IP地址和目的IP地址
IP在网络中,用来标识唯一的一台主机。
源 IP 地址:标识数据包的发送方主机。
目的 IP 地址:标识数据包的接收方主机。

通过局部到整体,数据包由主机A交到主机B,但最终还需要将数据交给进程处理。但一个主机中有非常多的进程,所以,就需要对目标进程进行标记。

3~> 认识端口号Port
端口号是一个2字节的16位整数,用来标识一台主机中唯一的进程。
3.1 端口号的范围划分
0 ~ 1023:系统端口/知名端口,HTTP,FTP,SSH等这些广为使用的应用层协议, 他们的端口号都是固定的。即约定这些端口号为特定的服务所占用,方便客户端同一访问,如报警打110。1024 - 65535:操作系统动态分配的端口号。客户端程序的端口号, 就是由操作系统从这个范围分配的。未来我们在进行套接字通信时指定的端口就在这个范围。
注意: 一个进程可以绑定多个端口号,但一个端口号只能被一个进程绑定!
3.2 理解socket
IP标识唯一的主机,端口号标识主机上唯一的进程。
那么 IP + Port即对应互联网中唯一的进程,即套接字。通过套接字,主机A上的进程A就能够把数据准确交给主机B上的进程B了。
3.3 端口号和进程PID
当我们创建一个进程,操作系统会自动为进程分配PID,PID标识唯一的进程。那为什么不直接用IP + PID?
首先,PID是系统概念,这样不符合解耦设计的分层哲学;其次,端口号将来需要被客户端明确知道,而端口号由客户端自己约定比较方便。
更何况不是所有进程都需要网络通信!
4 ~> 网络字节序
所有在网络中传输的多字节数据,必须使用大端序编码,这称为"网络字节序"。
4.1 为什么要规定网络字节序?
计算机存储多字节数据有两种顺序:例如存储(0x12345678)
- 大端存储:高字节位放在低地址处,即
12 34 56 78,符合我们的阅读习惯。 - 小端存储:高字节位存储在该地址处,即
78 56 34 12。
假如主机A(大端)想要将0x12345678发送给主机B(小端),最终主机B存的将是12 34 56 78,刚好相反。
所以,就需要一个标准,规定发送到网络中的必须是大端序列。大端机器正常读,小端机器在读之前先将大端 -> 小端即可。
4.2 网络字节序转化
主机将数据发送到网络,必须先转化为大端序:
htons(Host to Network Short):将端口号转化为网络字节序htonl(Host to Network Long):将IP转化为网络字节序
从网络读取的数据,必须转化为自身序列:
ntohs:将端口号由网络字节序转为主机序列ntohl:将IP由网络字节序转为主机序列
5 ~> Socket编程接口
Socket接口是应用层进入传输层的入口,即用户层通过系统调用(API)触发用户态与内核态之间的数据拷贝,从而间接完成网络数据的收发。
5.1 相关API介绍
(1)创建套接字(TCP/UDP,服务端+客户端)
int socket(int domain, int type, int protocol);
| 参数 | 含义 | 常用取值 |
|---|---|---|
domain | 协议族/地址族 | AF_INET (IPv4)、AF_INET6 (IPv6)、AF_UNIX (本地套接字) |
type | 套接字类型 | SOCK_STREAM (TCP)、SOCK_DGRAM (UDP)、SOCK_RAW (原始套接字) |
protocol | 具体协议:通常填 0,让系统根据 domain+type 自动选择 | IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP |
domain 协议族:
| 取值 | 说明 |
|---|---|
AF_INET | IPv4 地址,使用 sockaddr_in 结构体,地址是 32 位 |
AF_INET6 | IPv6 地址,使用 sockaddr_in6 结构体,地址是 128 位 |
type:套接字类型
| 取值 | 说明 | 对应协议 |
|---|---|---|
SOCK_STREAM | 面向连接、可靠、字节流、有序、无边界 | TCP |
SOCK_DGRAM | 无连接、不可靠、数据报、有边界、可能乱序/丢失 | UDP |
// ------------------代码示例-------------------------
// 创建一个 TCP/IPv4 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 创建一个 UDP/IPv4 套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
(2)绑定(TCP/UDP,服务端)
它把一个具体的地址(IP + 端口)绑定到套接字上,告诉操作系统:"这个端口归我管,有数据来就交给我。
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
| 参数 | 类型 | 作用 |
|---|---|---|
sockfd | int | socket() 创建的套接字文件描述符 |
addr | const struct sockaddr * | 指向地址结构体的指针(包含 IP + 端口) |
addrlen | socklen_t | addr 指向结构体的实际字节长度 |
(3)监听(TCP,服务端)
int listen(int socket, int backlog);
如果创建套接字和绑定是开饭馆和挂牌子,那么listen就是让饭馆开始营业,backlog代表饭馆能坐多少桌。所以只有监听后(即套接字处于listen状态)才能够接收数据,并放入队列等待处理。
(4)接收请求(TCP,服务端)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
服务端阻塞等待,当客户端发起连接connect,此时客户端accept接受请求,并返回一个全新的套接字描述符,专门用于与这个客户端通信。
address为输出型参数,存储了客户端的IP + Port。
(5)建立连接(TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
向服务器发起连接,addr存储了要连接的服务端的IP + Port。
5.2 结构体家族
- sockaddr结构体——基类
struct sockaddr
{
sa_family_t sa_family; // 地址族(如 AF_INET)
char sa_data[14]; // 存储具体地址数据(但空间不够,只是个占位符)
};
- sockaddr_in结构体——真正的 IPv4“身份证”
包含了IP和Port信息,就可以与对应进程进行数据交互。
struct sockaddr_in
{
sa_family_t sin_family; // 必须填 AF_INET
in_port_t sin_port; // 端口号(必须 htons 转换)
struct in_addr sin_addr; // IP 地址(必须 htonl 或 inet_pton 转换)
char sin_zero[8]; // 填充位,保证总长度与 sockaddr 一致
};
struct in_addr
{
in_addr_t s_addr; // 真正的IP
};
- sockaddr_un结构体——本地通信的“身份证”
struct sockaddr_un {
sa_family_t sun_family; // 必须填 AF_UNIX
char sun_path[108]; // 套接字文件的路径(如 "/tmp/my.sock")
};
struct sockaddr *address 是基类指针,使用sockaddr_in和sockaddr_un时需要强转。
5.2 传输层协议 UDP和TCP
5.2.1 认识UDP协议
- 无连接
- 不可靠传输
- 面向数据报
UDP通信流程:

-
客户端不需要
bind()(系统自动分配临时端口) -
数据报之间相互独立,可能丢包、乱序
5.2.2 认识TCP协议
- 有连接
- 可靠传输
- 面向字节流

-
服务端必须
bind + listen + accept三步曲 -
客户端必须
connect主动建立连接。 -
accept()返回的新套接字用于通信,原监听套接字继续等新连接。 -
TCP是字节流,无消息边界,需要应用层自行处理。
6 ~> 总结
网络编程的核心是跨主机的进程间通信,IP + Port 标识两个唯一的进程,通过 TCP/IP 协议栈完成数据交换。
理论准备完成,那么我们就可以接着进行网络通信实战了!
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/Miun123/article/details/162933425




