0. TCP\IP模型
1. 应用层
- 功能:
- 直接为用户应用程序提供网络服务(如文件传输、电子邮件、网页浏览)。
- 定义数据格式和会话规则(如HTTP的请求/响应模型)。
- 主要协议:
- HTTP/HTTPS:网页浏览。
- FTP:文件传输。
- SMTP/POP3/IMAP:电子邮件收发。
- DNS:域名解析。
- DHCP:动态IP分配。
- SSH/Telnet:远程登录。
2. 传输层
- 功能:
- 提供端到端(进程到进程)的数据传输服务。
- 保证可靠性(如TCP的重传机制)或高效性(如UDP的低延迟)。
- 通过**端口号**区分不同应用程序。
- 主要协议:
- TCP:面向连接、可靠传输(如网页、文件下载)。
- UDP:无连接、高效传输(如视频流、DNS查询)。
3. 网络层
- 功能:
- 负责数据包的路由选择和逻辑寻址(如IP地址)。
- 解决跨网络的通信问题(如路由器工作在这一层)。
- 主要协议:
- IP(IPv4/IPv6):数据包的路由和寻址。
- ICMP:网络状态检测(如`ping`)。
- ARP:IP地址 → MAC地址解析。
- RIP/OSPF/BGP:动态路由协议。
4. 数据链路层
- 功能:
- 在同一局域网内传输数据帧(通过MAC地址)。
- 提供错误检测(如CRC校验),但不纠正错误。
- 管理物理层设备(如交换机工作在这一层)。
- 主要协议/技术:
- Ethernet(IEEE 802.3):有线局域网。
- Wi-Fi(IEEE 802.11):无线局域网。
- PPP:点对点协议(如拨号上网)。
- VLAN(IEEE 802.1Q):虚拟局域网。
5. 物理层(Physical Layer)
- 功能:
- 定义物理介质(如电缆、光纤)的电气/光学特性。
- 传输原始比特流(0和1)。
关键点总结:
1. 应用层:用户交互,协议定义应用行为。
2. 传输层:进程间通信,选择可靠(TCP)或高效(UDP)。
3. 网络层:跨网络寻址和路由(IP是核心)。
4. 数据链路层:局域网内帧传输(MAC地址)。
5. 物理层:物理介质和比特流传输。 去掉文中的*和#
1. 网络传输基本流程
1.1 mac地址
(1)基本概念
每台主机在局域网上,有唯一的标识来保证主机的唯一性:mac 地址。
- 在网卡出厂时就确定了,不能修改。mac 地址通常是唯一的。
- MAC 地址用来识别数据链路层中相连的节点;
- 长度为 48 位, 即 6 个字节. 一般用 16 进制数字加上冒号的形式来表示(例如:08:00:27:03:fb:19)
(2)网络传输相关概念
- 以太网中,任何时刻,只允许一台机器向网络中发送数据
- 如果有多台同时发送,会发生数据干扰,我们称之为数据碰撞
- 所有发送数据的主机要进行碰撞检测和碰撞避免
- 没有交换机的情况下,一个以太网就是一个碰撞域
- 局域网通信的过程中,主机对收到的报文确认是否是发给自己的,是通过目标mac地址判定
以太网的本质就是共享的资源,也就是临界资源,那上面所说的数据碰撞不就是互斥属性吗!
(3)数据传输流程
每层都有协议,所以当进行上述传输流程的时候,要进行封装和解包
1.2 数据包封装和分用

下图为数据分用的过程
我们学习任何协议,都要先宏观上建立这样的认识:
1.要学习的协议,是如何做到解包的?只有明确了解包,封包也就能理解
2.要学习的协议,是如何做到将自己的有效载荷,交付给上层协议的?
1.3 IP地址
IP 协议有两个版本,IPv4 和 IPv6。下面默认指 IPv4。
- IP 地址是在 IP 协议中,用来标识网络中不同主机的地址;
- 对于 IPv4 来说,IP 地址是一个 4 字节,32 位的整数;
- 我们通常也使用 "点分十进制" 的字符串表示 IP 地址,例如 192.168.0.1;用点分割的每一个数字表示一个字节,范围是 0 - 255。
下面是一张跨网络传输流程图
路由器负责在不同网络之间转发数据,单层网络(如局域网)无法直接跨网通信。
目的IP的作用是明确数据包最终要到达的设备,并指导路由器如何转发。
- 目的 IP 是一种长远目标,Mac 是下一阶段目标,目的 IP 是路径选择的重要依据,mac 地址是局域网转发的重要依据,只在本局域网中有效。
2.socket编程预备
数据传输到主机是目的吗?不是的。因为数据是给人用的。比如:聊天是人在聊天,下载是人在下载,浏览网页是人在浏览。
但是人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?通过启动的QQ、迅雷、浏览器。而启动的QQ、迅雷、浏览器都是进程。换句话说,进程是人在系统中的代表,只要把数据给进程,人就相当于拿到了数据。
所以:数据传输到主机不是目的,而是手段。到达主机内部,再交给主机内的进程,才是目的。
但是系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程?这就要在网络的背景下,在系统中,标识主机的唯一性,也就是源 IP 地址和目的 IP 地址存在的意义。
网络通信的本质就是让两个不同主机的进程进行数据交互,即进程间通信, 让不同进程看到同一份资源。
2.1 端口号
端口号进程划分:
端口号与进程ID的区别:
- 一个进程可以绑定多个端口,但一个端口同一时间只能被一个进程占
- 端口号仅在网络通信期间有效,而进程ID从进程启动到结束一直存在
综上,IP 地址用来标识互联网中唯一的一台主机,port 用来标识该主机上唯一的一个网络进程,所以 IP+Port 就能表示互联网中唯一的一个进程。
3. 网络字节序
相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
大端模式(Big-Endian)
高字节存储在低地址,符合人类阅读习惯。
小端模式(Little-Endian)
低字节存储在低地址,兼容CPU计算方式(如x86/ARM)。
网络数据传输的字节序规则:
1. 数据发送与接收的地址顺序
- 发送主机:
- 将发送缓冲区中的数据按内存地址从低到高的顺序发出。
- 先发出的数据是低地址,后发出的数据是高地址。
- 接收主机:
- 把从网络上接收到的字节依次保存在接收缓冲区中,同样按内存地址从低到高的顺序保存。
2. 网络字节序规定
- TCP/IP协议规定,网络数据流采用大端字节序(低地址存高字节)。
- 无论主机是大端机还是小端机,都必须按照TCP/IP规定的网络字节序来发送/接收数据。
- 如果发送主机是小端机,需要先将数据转成大端序;如果是大端机则可直接发送。

h表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数。
例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
4.socket编程接口
C
// 创建 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);
4.1 sockaddr 结构
struct sockaddr {
sa_family_t sa_family; // 地址族(如 AF_INET)
char sa_data[14]; // 协议特定地址数据
};
在基于 IPv4 编程时,使用的数据结构是 sockaddr_in 和 sockaddr_un,但都需要强制转换为struct sockaddr *,这样不同协议族的地址结构能统一通过 socket API 传递。
sockaddr_in 结构里主要有三部分信息:地址类型,端口号,IP 地址
struct sockaddr_in {
sa_family_t sin_family; // 地址族(AF_INET)
in_port_t sin_port; // 16位端口号(网络字节序)
struct in_addr sin_addr; // 32位IP地址
unsigned char sin_zero[8]; // 填充字段(保证与sockaddr大小一致)
};
必须设置 sin_family = AF_INET(IPv4协议)。
sin_port 和 sin_addr.s_addr 需转换为网络字节序(htons()/htonl())。
sin_zero 仅用于填充,通常置零(memset(&addr, 0, sizeof(addr)))。
typedef uint16_t in_port_t; 无符号16位整数
typedef unsigned short int sa_family_t; 无符号短整数
typedef uint32_t in_addr_t; 无符号32位整数
in_addr结构

4.2 套接字相关函数
4.2.1 创建套接字:socket()
参数:
domain:地址族。
常用取值:
AF_INET IPv4 协议族(最常用),地址结构为 struct sockaddr_in。
AF_INET6 IPv6 协议族,地址结构为 struct sockaddr_in6。
AF_UNIX/AF_LOCAL 本地进程间通信(Unix Domain Socket),地址为文件路径。
AF_PACKET 底层数据包接口(如原始套接字,可监听网卡流量)。
type:套接字类型,UDP 使用 SOCK_DGRAM。
protocol:通常为 0(自动选择协议,UDP 对应 IPPROTO_UDP)。
当 protocol = 0 时,系统会根据 domain 和 type 自动选择协议:
AF_INET + SOCK_DGRAM → 自动选择 IPPROTO_UDP。
AF_INET + SOCK_STREAM → 自动选择 IPPROTO_TCP。
返回值:成功返回套接字描述符(sockfd),失败返回 -1。
在 Linux 编程中,套接字描述符(sockfd) 是一个整数,它是内核中套接字数据结构的引用。
这个数据结构包含:
本地地址和端口(通过 bind() 设置)。
远程地址和端口(通过 connect() 或 sendto() 设置)。
协议类型(如 UDP/TCP)。
缓冲区(用于存储待发送或已接收的数据)。
状态标志(如是否已连接、是否可读 / 写)。
4.4.2 绑定地址:bind()
将套接字绑定到本地IP和端口(服务端必选,客户端可选)。
参数:
sockfd:由 socket() 返回的套接字描述符,用于指定要绑定的套接字。
它是输入参数,表示 “对哪个套接字进行绑定操作”。
addr:指向 struct sockaddr_in(IPv4)或 sockaddr_in6(IPv6)的指针。
addrlen:地址结构体长度。
返回值:成功返回 0,失败返回 -1。
4.2.3 发送数据:sendto()
参数:
buf:待发送数据的缓冲区。
len:数据长度。
dest_addr:目标地址结构体。
addrlen:目标地址长度。
返回值:成功返回发送的字节数,失败返回 -1。
4.2.4 接受数据:recvfrom()
参数:
buf:接收数据的缓冲区。
len:缓冲区最大长度。
src_addr:用于保存发送方地址(可为 NULL)。
addrlen:输入时为缓冲区长度,输出时为实际地址长度。
返回值:成功返回接收的字节数,失败返回 -1。
4.2.5 TCP相关函数
int listen(int socket, int backlog);
用于服务器端:将一个已绑定地址的套接字设置为监听状态,准备接受客户端连接。
socket:文件描述符,由 socket() 创建。
backlog:内核为该套接字维护的 已完成连接队列 和 未完成连接队列 的长度上限。
成功时返回 0,失败时返回 -1 并设置 errno。
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
用于 服务器端:从监听套接字上接受一个连接,返回一个新的套接字,用于与客户端通信。
socket:监听套接字(由 listen() 设置的)。
address:指向结构体,用于获取客户端的地址信息。
address_len:输入输出参数,指定结构体大小并返回实际填充的大小。
返回值:新的连接套接字;若出错返回 -1。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用于 客户端:主动发起连接请求到服务器端指定地址,将套接字 sockfd 连接到远程服务器。
sockfd:由 socket() 创建的套接字。
addr:指向服务器地址结构体,如 sockaddr_in。
addrlen:地址结构体的大小。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv() 是用于从已连接的套接字接收数据的系统调用,主要用于 TCP 通信。
lags: 控制接收行为的标志位(常用值如下):
0: 默认行为(阻塞模式)
MSG_PEEK: 查看数据但不从接收队列移除
MSG_WAITALL: 等待直到请求的所有数据都到达
MSG_DONTWAIT: 非阻塞接收
返回值
成功时:返回接收到的字节数
连接关闭:返回 0(对端正常关闭连接)
出错时:返回 -1,并设置 errno
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
send() 是用于通过已连接的套接字发送数据的系统调用,主要用于 TCP 通信。
flags: 控制发送行为的标志位(常用值如下):
0: 默认行为(阻塞模式)
MSG_DONTWAIT: 非阻塞发送
MSG_NOSIGNAL: 发送失败时不产生 SIGPIPE 信号
MSG_OOB: 发送带外数据(紧急数据)
返回值
成功时:返回实际发送的字节数(可能小于请求的 len)
出错时:返回 -1,并设置 errno
4.2.6 其他辅助函数
地址转换
(1)inet_pton() 是一个用于将 人类可读的IP地址字符串 转换为 二进制网络字节序 的函数,通常在套接字编程中用于设置 struct sockaddr_in 或 struct sockaddr_in6 的地址字段。
n 代表 Network(网络),即二进制形式的 IP 地址(如 struct in_addr 或 struct in6_addr)。
p 代表 Presentation(表示),即人类可读的字符串形式(如 "192.168.1.1" 或 "2001:db8::1")。
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
af地址族:AF_INET(IPv4), AF_INET6(IPv6)
const char* src:源字符串,即点分十进制的IPv4地址(如 "192.168.1.1")或IPv6地址。
dst:目标缓冲区指针,用于存储转换后的二进制地址:
IPv4:struct in_addr
IPv6:struct in6_addr
返回值
1 转换成功,结果存储在 dst 中。
0 输入的地址字符串不合法(与 af 指定的地址族不匹配)。
-1 错误发生(如地址族不支持),可通过 errno 获取具体错误原因。
(2)inet_ntop()
:将二进制IP转换为字符串形式。
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数size表示缓冲区 dst
的大小(防止溢出)。
字节序转换:
htons()、htonl():主机字节序 → 网络字节序(大端)。
ntohs()、ntohl():网络字节序 → 主机字节序。
4.3 网络命令
1. ifconfig(Interface Config)
用于查看和配置网络接口信息。
2. netstat(Network Statistics)
用于显示网络连接、路由表、接口状态等信息(被 ss 命令取代,但仍可用)。
-t:仅显示TCP监听状态的连接
-u:仅显示UDP监听状态的连接
-l:仅显示所有监听状态(Listen)的连接
-n:拒绝显示别名,能显示数字的全部转化成数字
-p:显示建立相关链接的程序名
3. ping
用于测试网络连接性和主机可达性。通过向目标主机发送 ICMP(Internet Control Message Protocol,互联网控制消息协议 )回显请求数据包,等待目标主机返回 ICMP 回显应答数据包,以此判断网络是否畅通、延迟情况等。
-c :指定发送 ICMP 请求包的数量。
-w :设置等待响应的超时时间(单位为秒)。
4. pidof
ps axj | head -1 && ps ajx | grep tcp_server
PPID PID PGID SID TTY TPGID STAT UID TIME
COMMAND
2958169 2958285 2958285 2958169 pts/2 2958285 S+ 1002
0:00 ./tcp_server 8888
pidof tcp_server
2958285
4. telnet
用于测试端口连通性,尤其是 TCP 端口。
telnet [IP地址] [端口号]
例子:测试本机的 80 端口是否可连接:
telnet 127.0.0.1 80
如果连接成功,会看到类似于:
Trying 127.0.0.1...
Connected to 127.0.0.1.
如果没有安装 telnet,可以用:
nc -vz 127.0.0.1 80
UDP编程流程
服务端
socket() → bind() → recvfrom()/sendto() → close()。
客户端
socket() → sendto()/recvfrom() → close()(无需 bind,系统自动分配端口)。
5. UDPsocket的封装
UdpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
using namespace LogModule;
const int defaultfd = -1;
using func_t = std::function<std::string(const std::string &)>;
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)
: _sockfd(defaultfd),
//_ip(ip),
_port(port),
_isrunning(false),
_func(func)
{
}
void Init()
{
// 1. 创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
LOG(LogLevel::FATAL) << "socket error";
exit(1);
}
LOG(LogLevel::INFO) << "socket create success" << _sockfd;
//2. 绑定接口 本地ip和端口
//2.1 填充sockaddr_in结构体
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
//本地格式-》网络序列
local.sin_port = htons(_port);
//IP转换为网络序列
//local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_addr.s_addr = INADDR_ANY; //宏对应的值是0
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if(n == -1)
{
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::INFO) << "bind success, sockfd" << _sockfd;
}
void Start()
{
_isrunning = true;
while(_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//1. 收消息
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
int peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
buffer[s] = 0;
//LOG(LogLevel::INFO) << "[" << peer_ip << ":" << peer_port << "buffer:" << buffer;
LOG(LogLevel::INFO) << "buffer:" << buffer;
std::string result = _func(buffer);
//2. 发消息
//std::string echo_string = "server echo@";
//echo_string += buffer;
sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer() {}
private:
int _sockfd;
uint16_t _port;
//std::string _ip; // 用的是字符串风格,点分十进制
bool _isrunning;
func_t _func;
};
UdpServer.cc
#include <iostream>
#include <memory>
#include <string>
#include "UdpServer.hpp"
std::string defaulthandler(const std::string &str)
{
std::string ret = "hello, ";
ret += str;
return ret;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage:" << argv[0] << " port" << std::endl;
return 1;
}
//std::string ip = argv[1];
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Strategy();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler);
usvr->Init();
usvr->Start();
return 0;
}
UdpClient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
int main(int argc, char *argv[])
{
if(argc != 3)
{
std::cerr << "Usage:" << argv[0] << "server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
std::cout << "server_ip: " << server_ip << " server_port: " << server_port <<std::endl;
//1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
std::cout << "sockfd: " << sockfd << std::endl;
//客户端无需显示低绑定,首次发送消息,客户端会自动给client进行bind
//,OS知道IP,采用随机端口号的方式,client的端口号只要是唯一的就行
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
std::cout << "sin_port: " << server.sin_port << " sin_addr: " << server.sin_addr.s_addr << std::endl;
while(true)
{
std::string input;
std::cout << "Please Enter:";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}