计算机网络
网络基础
重要概念
[!TIP]
局域网(LAN):有限地理范围(如办公室、校园),常见技术有以太网,路由器,交换机,Wi-Fi。
广域网(WAN):跨越地理区域(城市/国家),常见技术有 VPN 等。
MAC地址用于数据链路层的通信,主要用于局域网内部的设备识别和数据帧传输。
IP地址用于网络层的通信,可以在局域网内部或跨越多个网络(如互联网)进行通信。
IP/端口号
[!NOTE]
端口号是一个2字节16位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
一个端口号只能被一个进程占用
基于IP + port
的通信方式我们称作** socket
**。思考为什么不用 pid
而使用port
?
- 不是所有进程都需要通信
- 系统和网络功能解耦
进程和端口号的对应关系:一个进程可以对应多个端口号
TCP和UDP
TCP(传输控制协议)面向字节流(Stream-oriented)。保证数据的可靠传输。发送方和接收方通过TCP协议建立连接后,数据会被封装成多个有序的报文段(Segment),并在传输过程中进行错误检测、确认、重传和流量控制。TCP确保数据的完整性、顺序性和可靠性。适用于对数据可靠性要求较高的场景,如文件传输(FTP)、网页浏览(HTTP/HTTPS)、电子邮件(SMTP/POP3)等。
UDP(用户数据报协议):面向数据报(Datagram-oriented)。UDP将数据封装成独立的数据报(Datagram),每个数据报独立传输,不保证顺序、可靠性或完整性。UDP不建立连接,也不进行错误检测和重传机制,因此传输速度快,但可靠性较低。适用于对实时性要求较高且可以容忍一定数据丢失的场景,如视频流媒体(RTP)、语音通话(VoIP)。
网络字节序
TCP/IP协议决定网络数据流采用大端字节序,即低地址高字节。
[!IMPORTANT]
复习一下大小端
假设有一个16位的整数 0x1234
,在大端字节序中,0x12
(高字节)会被存储在低地址位置,0x34
(低字节)会被存储在高地址位置。
OSI与TCP/IP模型
网络层与传输层是Linux操作系统中模块的一部分。
网络通信的本质:就是贯穿协议栈的过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| graph TD A[OSI七层模型] -->|实际工程实现| B[TCP/IP四层模型] B --> C[Linux协议栈实现] subgraph OSI模型 L7[应用层] --> L6[表示层] L6 --> L5[会话层] L5 --> L4[传输层] L4 --> L3[网络层] L3 --> L2[数据链路层] L2 --> L1[物理层] end subgraph TCP/IP模型 T4[应用层] --> T3[传输层] T3 --> T2[网络层] T2 --> T1[链路层] end
|
[!IMPORTANT]
TCP/IP 四层模型
- 应用层:这是最接近用户的一层,负责处理特定的应用程序需求,比如网页浏览(HTTP)、邮件(SMTP)等。
- 传输层:这一层负责端到端的数据传输,TCP和UDP就在这里工作。它们决定数据如何从一台设备传输到另一台设备。
- 网络层:这一层负责将数据从源设备传输到目标设备,主要协议是IP(Internet Protocol)。
- 链路层:这一层负责在物理设备之间传输数据,比如以太网(Ethernet)。
以太网:以太网是一种广泛应用于局域网(LAN)的网络技术,用于实现同一网络环境内设备之间的数据传输和通信。在以太网中,同一局域网内的任意两台主机可以通过物理链路(如双绞线、光纤)直接通信,无需经过外部网络的中转。
数据从上层协议(如IP协议)传递到以太网时,会在每一层添加相应的报头(Header)。
报文的结构:报文=报头+有效载荷
数据的封装与解包:网络通信过程中,数据在发送方从应用层向下传递到链路层,每经过一层都会被封装上该层的协议头(Header)。例如:
- 应用层数据被封装为TCP或UDP数据报。
- TCP/UDP数据报被封装为IP数据包。
- IP数据包被封装为以太网帧。
在计算机网络的每一层协议中,通常需要具备将报头(Header)与有效载荷(Payload)分离的能力。每一层协议通常需要在其报头中提供一种机制,用于决定将有效载荷传递给上层的哪一个协议。(在TCP/IP协议栈中,IP报头中的“协议”字段(Protocol Field)用于标识上层协议(如TCP、UDP等))
交换机:以太网发生数据碰撞问题(碰撞避免算法),现代网络设计中广泛采用交换机来有效降低碰撞概率。交换机通过划分碰撞域(Collision Domain),将每个端口划分为独立的冲突域,从而显著减少数据碰撞的可能性
IP协议通过屏蔽底层网络的差异化,实现了不同网络之间的无缝互连。 这一功能的实现主要依赖于工作在IP层的路由器。路由器作为网络层的核心设备,通过解析IP地址,实现数据包在不同网络之间的转发和路由选择,从而支持跨网络的通信。

网络编程套接字
网络通信的本质:进程的通信
网络套接字(Socket)是计算机网络中不同主机之间通信的端点,是实现进程间通信的关键技术。
常见 API 分析
socket()
1
| int socket(int domain, int type, int protocol);
|
作用:创建通信端点
参数:
domain
:地址族(AF_INET表示IPv4)
type
:通信语义(SOCK_DGRAM表示UDP)
protocol
:协议类型(0表示自动选择)
格式:
1
| int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
|
bind()
1
| int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
作用:将套接字与地址/端口绑定
关键数据结构:
1 2 3 4 5
| struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; };
|
格式:
1 2
| server.sin_port = htons(serverport); server.sin_addr.s_addr = inet_addr(serverip.c_str());
|
sendto()
/recvfrom()
1 2 3 4 5
| ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
|
- 特点:面向无连接的收发接口
- 参数:
buf
:数据缓冲区
src_addr
/dest_addr
:自动获取/指定通信方地址
addrlen
:地址结构体长度指针
- 字节序转换函数
函数 |
作用 |
示例 |
htons() |
主机字节序→网络字节序(端口) |
htons(8080) |
ntohs() |
网络字节序→主机字节序(端口) |
ntohs(server.sin_port) |
inet_addr() |
字符串IP→网络字节序 |
inet_addr("192.168.1.1") |
inet_ntoa() |
网络字节序→字符串IP |
inet_ntoa(client.sin_addr) |
[!TIP]
IP地址选择:
- 服务器绑定
0.0.0.0
表示监听所有网络接口
- 客户端连接时使用服务器内网IP(如
192.168.x.x
)
端口管理:
- 服务器需要指定固定端口(>1024)
- 客户端端口由系统自动分配
实验:搭建UDP服务器并在客户端打印处理结果
调试事项
需要注意的是:在同一台电脑上进行测试时,连接主机时候记得使用内网 IP 而非公网 IP
[!TIP]
netstat -nlup
是一个用于显示网络连接、路由表、接口统计信息、伪装连接和多播成员的命令。具体来说:
-n
:显示数字地址和端 口号,而不是尝试解析主机名和服务名。
-l
:显示监听中的套接字。
-u
:显示 UDP 协议的连接。
-p
:显示使用套接字的进程 ID 和进程名称。
1 2 3 4
| netstat -nlup
netstat -ant
|

服务器端(udpServer
):绑定指定端口持续监听;接收客户端数据并打印;添加”server echo#”前缀后返回给客户端。
客户端(udpClient
):连接指定IP和端口的服务器;交互式发送用户输入的消息;接收并打印服务器响应
运行结果:

启动服务器:
启动客户端:
1 2
| ./udpclient 127.0.0.1 8080 ./udpclient 192.168.1.100 8080
|
交互过程:
1 2
| client say: Hello server echo# Hello
|
实现代码
Main.cc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| # include "udpServer.hpp" # include <memory>
void Usage(std::string proc) { std::cout << "Usage: " << proc << " port[1024+]" << std::endl; }
int main(int argc, char *argv[]) { if(argc != 2){ Usage(argv[0]); exit(1); } uint16_t port = atoi(argv[1]);
std::unique_ptr<udpServer> svr(new udpServer(port));
svr -> Init(); svr -> Run(); return 0; }
|
udpClient.cc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| # include <iostream> # include <sys/types.h> # include <sys/socket.h> # include <netinet/in.h> # include <arpa/inet.h> # include <unistd.h> # include <cstdlib> # include <strings.h> # include <cstring>
using namespace std;
void Usage(std::string proc) { std::cout << "Usage: " << proc << " ServerIP + ServerPort" << std::endl; }
int main(int argc, char *argv[]) { if (argc != 3){ Usage(argv[0]); exit(1); }
std::string serverip = argv[1]; uint16_t serverport = atoi(argv[2]);
struct sockaddr_in server; bzero(&server, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport); server.sin_addr.s_addr = inet_addr(serverip.c_str()); socklen_t len = sizeof(server); int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd < 0){ cerr << "socket error: " << strerror(errno) << endl; exit(1); } else{ cout << "socket success" << endl; }
string message; char buffer[1024]; while(true) { cout << "client say: "; getline(cin, message);
ssize_t sent_bytes = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len); if (sent_bytes < 0) { cerr << "sendto error: " << strerror(errno) << endl; continue; } else { cout << "sendto success, sent " << sent_bytes << " bytes to " << inet_ntoa(server.sin_addr) << ":" << ntohs(server.sin_port) << endl; }
struct sockaddr_in tmp; socklen_t len = sizeof(tmp);
size_t s = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &len); if(s > 0){ buffer[s] = 0; cout << buffer << endl; } else { cerr << "recvfrom error: " << strerror(errno) << endl; } }
close (sockfd); return 0; }
|
udpServer.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| # pragma once # include <iostream> # include <sys/types.h> # include <sys/socket.h> # include <netinet/in.h> # include <arpa/inet.h> # include <cstring> # include <unistd.h>
uint16_t defaultPort = 8080; std::string defaultIP = "0.0.0.0";
class udpServer{ public: udpServer(const uint16_t &port = defaultPort, const std::string &ip = defaultIP) : _sockfd(-1), _port(port), _ip(ip), _isrunning(false) { }
void Init(){ _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(_sockfd < 0){ std::cerr << "socket error" << std::endl; exit(1); } else{ std::cout << "socket success" << std::endl; } struct sockaddr_in local; bzero(&local, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = inet_addr(_ip.c_str());
int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local)); if(n < 0){ std::cerr << "bind error: " << strerror(errno) << std::endl; exit(2); } else{ std::cout << "bind success" << std::endl; } }
void Run(){ _isrunning = true; while(_isrunning) { struct sockaddr_in client; socklen_t len = sizeof(client);
char inbuffer[1024]; ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len); if (n < 0){ std::cerr << "recvfrom error: " << strerror(errno) << std::endl; continue; } else{ std::cout << "recvfrom success, received " << n << " bytes from " << inet_ntoa(client.sin_addr) << ":" << ntohs(client.sin_port) << std::endl; } inbuffer[n] = 0;
std::string info = inbuffer; std::string echo_string = "server echo# " + info; std::cout << "server received: " << info << std::endl; ssize_t sent_bytes = sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len); if (sent_bytes < 0) { std::cerr << "sendto error: " << strerror(errno) << std::endl; } else { std::cout << "sendto success, sent " << sent_bytes << " bytes to " << inet_ntoa(client.sin_addr) << ":" << ntohs(client.sin_port) << std::endl; } }
}
~udpServer(){ if(_sockfd != 0){ close(_sockfd); } } private: int _sockfd; std::string _ip; uint16_t _port; bool _isrunning; };
|
makefile
1 2 3 4 5 6 7 8 9 10
| .PHONY:all all: udpserver udpclient udpclient: udpClient.cc g++ -o $@ $^ -std=c++11 udpserver: Main.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f udpserver udpclient
|