前言:
本文是 Linux 网络编程 Day01 的学习笔记,涵盖了网络通信的基础概念、OSI 七层模型、TCP/IP 协议栈,以及基于 TCP 的 Socket 编程实战。
一、为什么要学网络编程?
1.1 进程间通信(IPC)的两大场景
在 Linux 系统编程中,进程间通信(IPC)分为两类:
同一主机内的通信:
| 类型 | 方式 |
|---|---|
| 传统方式 | 有名管道、无名管道、信号 |
| System V IPC | 共享内存、信号量、消息队列 |
不同主机间的通信:
| 类型 | 方式 |
|---|---|
| 网络通信 | Socket(套接字) |
核心问题:如何解决不同主机间两个进程的通信问题?
这个问题需要从两个层面来解决:
- 物理通路:线路保障连接(光纤、网线、无线、5G)
- 逻辑通路:从编程(软件)层面建立一条通路
二、网络基础概念
2.1 早期网络:ARPA(阿帕网)
早期的 ARPA 网络使用 NCP(Network Control Protocol)协议,存在三个问题:
- 1.不支持不同类型的操作系统
- 2.不支持不同类型的计算机之间的通信
- 3.不支持纠错功能
2.2 OSI 七层模型
为了解决不同体系结构网络的互联问题,国际标准化组织 ISO 于 1981 年制定了 OSI/RM(Open System Interconnection Reference Model)------开放系统互连参考模型。
说明:OSI 是理想模型,尚未完全实现。
每一层都有三个要素:功能 、协议 (通信双方规定的格式)、设备。
┌─────────────────────────────────────────────┐
│ 7. 应用层 │ 最靠近用户,提供网络服务(邮件、文件传输等) │
├─────────────────────────────────────────────┤
│ 6. 表示层 │ 数据格式化、加密解密(gzip) │
├─────────────────────────────────────────────┤
│ 5. 会话层 │ 连接状态管理(长连接、短连接、keep-alive) │
├─────────────────────────────────────────────┤
│ 4. 传输层 │ 传输控制协议(TCP / UDP) │
├─────────────────────────────────────────────┤
│ 3. 网络层 │ 路由器,路径选择,IP 寻址 │
├─────────────────────────────────────────────┤
│ 2. 数据链路层 │ 交换机,帧传输,差错控制 │
├─────────────────────────────────────────────┤
│ 1. 物理层 │ bit 流传输,电气特性,网线/光纤标准 │
└─────────────────────────────────────────────┘
各层详细说明:
| 层 | 功能 | 数据单位 | 关键词 |
|---|---|---|---|
| 物理层 | 定义物理设备标准,传输比特流(数模/模数转换) | 比特(bit) | 网线、光纤、高低电平 |
| 数据链路层 | 帧格式化传输、错误检测和纠正 | 帧(frame) | 交换机、全双工、校验 |
| 网络层 | 不同地理位置的主机之间的连接和路径选择 | 数据段/包 | 路由器、IP 地址 |
| 传输层 | 数据分段传输和重组,定义传输协议和端口号 | 段(segment) | TCP、UDP、端口号 |
| 会话层 | 通过端口号建立数据传输通路,发起/接受会话请求 | - | 会话管理、keep-alive |
| 表示层 | 确保不同系统应用层信息可互相读取,数据格式转换 | - | 编码转换(ASCII/BCD) |
| 应用层 | 为用户应用程序提供网络服务 | 报文(datagram) | HTTP、FTP、DNS |
2.3 TCP/IP 四层模型(实际使用)
TCP/IP 模型是实用模型,是工业标准:
┌─────────────────────────────────────────────┐
│ 应用层 │ 应用程序(HTTP、FTP、DNS...) │
├─────────────────────────────────────────────┤
│ 传输层 │ 端口号(TCP / UDP) │
├─────────────────────────────────────────────┤
│ 网络层 │ IP 地址 │
├─────────────────────────────────────────────┤
│ 网络接口层 │ 网卡、驱动 │
└─────────────────────────────────────────────┘
还有一种非标准的 五层模型(OSI 与 TCP/IP 的折中):
应用层 → 传输层 → 网络层 → 数据链路层 → 物理层
2.4 TCP/IP 协议族
| 层 | 常见协议 |
|---|---|
| 应用层 | HTTP、TFTP、FTP、SNMP、DNS |
| 传输层 | TCP、UDP |
| 网络层 | IP、ICMP(ping)、RIP、OSPF、IGMP |
| 网络接口层 | ARP、RARP |
2.5 网络地址
IP 地址:
-
网络中主机的唯一标识,由网络地址 + 主机号组成
-
IPv4 中 IP 地址是 32 位二进制数,采用大端字节序
-
表示方法:点分十进制 (如
192.168.1.1)192.168.1.1
= 11000000.10101000.00000001.00000001
子网掩码: 用来区分网络号,也可以获取主机号。
端口号: 用来标识一个网络进程。
- 在网络中找到主机 → 靠 IP 地址
- 找到主机中的进程 → 靠端口号
字节序:
| 类型 | 说明 |
|---|---|
| 大端 | 高字节存低地址 |
| 小端 | 低字节存低地址 |
| 网络字节序 | 大端字节序 |
常用转换函数:
htons()------ 将端口号转换为网络字节序inet_addr()------ 将 IP 地址转换为网络字节序
三、TCP 与 UDP 协议
3.1 TCP 协议
TCP(Transmit Control Protocol),类似打电话。
三大特点:
- 1.面向连接:通信前必须建立逻辑通路
- 2.可靠传输:无丢失、无失序、无差错、无重复
- 3.字节流传输
TCP 三次握手(建立连接):
客户端 服务器
| |
| ---- 1. 我要通话 (SYN) ---> | 客户端发送连接请求
| |
| <-- 2. 好的,知道了 (SYN+ACK) | 服务器确认,可以通信
| |
| ---- 3. 嗯,好的 (ACK) ---> | 客户端确认,连接建立
| |
为什么是三次而不是两次或四次?------ 三次握手才能实现可靠的连接。
应用场景:
- 1.对可靠性要求较高的场合
- 2.QQ、微信等软件的登录过程
3.2 UDP 协议
UDP(User Datagram Protocol),类似生活中的广播。
三大特点:
- 1.无连接 ------ 不需要建立连接
- 2.不可靠传输 ------ 不保证数据到达
- 3.数据报 ------ 一包一包发送
应用场景:
- 1.小数据,实时性要求高(QQ 文本消息、语音)
- 2.广播、组播
- 3.无线网络传输
3.3 TCP vs UDP 对比
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(无丢失/失序/差错/重复) | 不可靠 |
| 传输方式 | 字节流 | 数据报 |
| 速度 | 相对较慢 | 快 |
| 典型应用 | 文件传输、登录认证 | 聊天消息、音视频、游戏 |
四、网络编程模型
4.1 三种模型
| 模型 | 全称 | 说明 |
|---|---|---|
| CS 模型 | Client/Server | 客户端/服务器模型(重点) |
| BS 模型 | Browser/Server | 浏览器/服务器模型 |
| P2P 模型 | Peer to Peer | 点对点模型 |
4.2 Linux 中 Socket 的位置
┌──────────────────────────────┐
│ 用户空间 │ 应用层(应用程序)│
├──────────────────────────────┤
│ │ =============== │
│ 内核空间 │ socket 接口层 │
│ │ 传输层 │
│ │ 网络层(协议栈) │
│ │ 链路层 │
│ │ 物理层 │
└──────────────────────────────┘
Socket 的两层含义:
- 1.是一个函数接口 ------ 用来使用网络功能
- 2.遵循 Linux "一切皆文件"的思想 ------ socket 也是一种特殊的文件,专门用来进行网络通信
五、TCP CS 通信模型
5.1 通信过程(两个阶段)
阶段一:建立连接 阶段二:通信
5.2 客户端流程
1. socket() // 买手机
2. connect() // 拨打电话(连接服务器)
3. read()/write() // 通信
4. close() // 挂电话
bind()对客户端是可选的,一般不绑定。
5.3 服务器端流程
1. socket() // 买手机
2. bind() // 插 SIM 卡(绑定服务器地址信息)
3. listen() // 待机(监听是否有客户端连接)
4. accept() // 接听电话(处理客户端的连接请求,完成连接建立)
5. read()/write() // 通信
6. close()
注意: 客户端只有 1 个 socket;服务器有 2 个 socket:一个用于监听,一个用于通信。
5.4 核心 API 详解
socket() ------ 创建通信端
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
| 参数 | 取值 | 说明 |
|---|---|---|
| domain | AF_UNIX / AF_LOCAL |
本地通信 |
AF_INET |
IPv4 网络通信 | |
AF_INET6 |
IPv6 网络通信 | |
| type | SOCK_STREAM |
流式套接字(TCP) |
SOCK_DGRAM |
数据报套接字(UDP) | |
| protocol | 0 |
回 socket 文件描述符,失败返回 -1 并设置 errno
connect() ------ 发起连接请求(客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 返回值:成功返回
0,失败返回-1并设置errno
bind() ------ 绑定地址信息(服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:给 sockfd 绑定一个名字(地址信息)
- 返回值:成功返回
0,失败返回-1并设置errno
listen() ------ 设置监听(服务器)
int listen(int sockfd, int backlog);
- 功能:将 sockfd 设置为监听套接字(被动套接字)
backlog:等待队列的大小- 返回值:成功返回
0,失败返回-1并设置errno
accept() ------ 接受连接(服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:从 listen 的等待队列中提取第一个连接请求,完成连接,创建已连接的 socket 并返回其 fd
addrlen:值-结果参数 ,必须初始化为sizeof(客户端地址类型)- 返回值:成功返回已连接好的 socket 的 fd,失败返回
-1
地址结构体
struct sockaddr_in {
sa_family_t sin_family; // 地址族:AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP 地址
};
struct in_addr {
in_addr_t s_addr; // IP 地址(网络字节序)
};
辅助函数
// 将主机字节序的端口号转换为网络字节序
uint16_t htons(uint16_t hostshort);
// 将点分十进制 IP 字符串转换为网络字节序的 IP 地址
in_addr_t inet_addr(const char *cp);
// 将网络字节序的 IP 地址转换为点分十进制字符串
char *inet_ntoa(struct in_addr in);
六、代码实战
6.1 基础版本:服务器接收,客户端发送
服务端 server_recv.c
cs
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//1.创建通信端
int fd = socket(AF_INET,SOCK_STREAM,0);
if (fd < 0)
{
perror("socket fail");
return -1;
}
printf("fd = %d\n",fd);
struct sockaddr_in seraddr; //服务器地址
bzero(&seraddr,sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.10.129");
//2.绑定服务器地址信息
if (bind(fd,(const struct sockaddr*)&seraddr,sizeof(seraddr)) < 0)
{
perror("connect fail");
return -1;
}
//3.设置监听
if (listen(fd,5) < 0)
{
perror("listen fail");
return -1;
}
struct sockaddr_in cliaddr;
socklen_t addrlen = sizeof(cliaddr);
int connfd=accept(fd, ( struct sockaddr *)&cliaddr, &addrlen);
//4.获取连接请求 建立 已连接的socket
//int connfd = accept(fd,NULL,NULL);
if (connfd < 0)
{
perror("accept fail");
return -1;
}
printf("accept success\n");
printf("-------------------\n");
printf("cliaddr ip = %s\n",inet_ntoa(cliaddr.sin_addr));
printf("port = %d\n", ntohs(cliaddr.sin_port));
printf("-------------------\n");
char buf[1024] = {0};
while (1)
{
read(connfd,buf,sizeof(buf));
printf("buf = %s\n",buf);
}
return 0;
}
客户端 client_send.c:
cs
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//1.创建通信端
int fd = socket(AF_INET,SOCK_STREAM,0);
if (fd < 0)
{
perror("socket fail");
return -1;
}
printf("fd = %d\n",fd);
struct sockaddr_in seraddr; //服务器地址
bzero(&seraddr,sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.10.129");
//2.建立连接
if (connect(fd,(const struct sockaddr*)&seraddr,sizeof(seraddr)) < 0)
{
perror("connect fail");
return -1;
}
write(fd,"hello tcp!\n",11);
return 0;
}
6.2 进阶版本:服务器回显消息
服务端 server_echo.c:
cs
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, const char *argv[])
{
//1.创建通信端
int fd = socket(AF_INET,SOCK_STREAM,0);
if (fd < 0)
{
perror("socket fail");
return -1;
}
printf("fd = %d\n",fd);
struct sockaddr_in seraddr; //服务器地址
bzero(&seraddr,sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.10.129");
//2.绑定服务器地址信息
if (bind(fd,(const struct sockaddr*)&seraddr,sizeof(seraddr)) < 0)
{
perror("connect fail");
return -1;
}
//3.设置监听
if (listen(fd,5) < 0)
{
perror("listen fail");
return -1;
}
struct sockaddr_in cliaddr;
socklen_t addrlen = sizeof(cliaddr);
int connfd=accept(fd, ( struct sockaddr *)&cliaddr, &addrlen);
//4.获取连接请求 建立 已连接的socket
if (connfd < 0)
{
perror("accept fail");
return -1;
}
printf("accept success\n");
printf("-------------------\n");
printf("cliaddr ip = %s\n",inet_ntoa(cliaddr.sin_addr));
printf("port = %d\n", ntohs(cliaddr.sin_port));
printf("-------------------\n");
char buf[1024] = {0};
while (1)
{
read(connfd,buf,sizeof(buf));
char sbuf[1024] = {0};
sprintf(sbuf,"server + %s\n",buf);
write(connfd,sbuf,strlen(sbuf)+1);
}
return 0;
}
客户端 client_echo.c:
cs
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, const char *argv[])
{
//1.创建通信端
int fd = socket(AF_INET,SOCK_STREAM,0);
if (fd < 0)
{
perror("socket fail");
return -1;
}
printf("fd = %d\n",fd);
struct sockaddr_in seraddr; //服务器地址
bzero(&seraddr,sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.10.129");
//2.建立连接
if (connect(fd,(const struct sockaddr*)&seraddr,sizeof(seraddr)) < 0)
{
perror("connect fail");
return -1;
}
char buf[1024] = {0};
while (1)
{
printf("> ");
fgets(buf,sizeof(buf),stdin);
write(fd,buf,strlen(buf)+1);
read(fd,buf,sizeof(buf));
printf("recv: %s\n",buf);
}
return 0;
}
6.3 点对点聊天版本(fork 多进程)
思路:使用 fork() 创建子进程,父进程负责发送消息 ,子进程负责接收消息,实现双向通信。
客户端 client_chat.c:
c
cs
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
int main(int argc, const char *argv[])
{
//1.创建通信端
int fd = socket(AF_INET,SOCK_STREAM,0);
if (fd < 0)
{
perror("socket fail");
return -1;
}
printf("fd = %d\n",fd);
struct sockaddr_in seraddr; //服务器地址
bzero(&seraddr,sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.10.129");
//2.建立连接
if (connect(fd,(const struct sockaddr*)&seraddr,sizeof(seraddr)) < 0)
{
perror("connect fail");
return -1;
}
char buf[1024] = {0};
while (1)
{
printf("> ");
fgets(buf,sizeof(buf),stdin);
write(fd,buf,strlen(buf)+1);
}
return 0;
}
服务端 server_chat.c:
cs
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//1.创建通信端
int fd = socket(AF_INET,SOCK_STREAM,0);
if (fd < 0)
{
perror("socket fail");
return -1;
}
printf("fd = %d\n",fd);
struct sockaddr_in seraddr; //服务器地址
bzero(&seraddr,sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.10.129");
//2.绑定服务器地址信息
if (bind(fd,(const struct sockaddr*)&seraddr,sizeof(seraddr)) < 0)
{
perror("connect fail");
return -1;
}
//3.设置监听
if (listen(fd,5) < 0)
{
perror("listen fail");
return -1;
}
struct sockaddr_in cliaddr;
socklen_t addrlen = sizeof(cliaddr);
int connfd=accept(fd, ( struct sockaddr *)&cliaddr, &addrlen);
//4.获取连接请求 建立 已连接的socket
if (connfd < 0)
{
perror("accept fail");
return -1;
}
printf("accept success\n");
printf("-------------------\n");
printf("cliaddr ip = %s\n",inet_ntoa(cliaddr.sin_addr));
printf("port = %d\n", ntohs(cliaddr.sin_port));
printf("-------------------\n");
char buf[1024] = {0};
while (1)
{
read(connfd,buf,sizeof(buf));
printf("buf = %s\n",buf);
}
return 0;
}
双向聊天的 fork 设计模式:
text
客户端/服务器 均使用 fork:
├── 父进程 → 负责发送消息
└── 子进程 → 负责接收消息
这样收发互不阻塞,实现真正的聊天。
6.4 字节序转换示例
test_htons.c:
cs
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, const char *argv[])
{
unsigned short a = 0x1234;
int ret = htons(a);
printf("ret = %#x\n",ret);
return 0;
}
七、编译与运
cs
# 编译
gcc server_recv.c -o server_recv
gcc client_send.c -o client_send
gcc server_echo.c -o server_echo
gcc client_echo.c -o client_echo
# 运行顺序:先启动服务器,再启动客户端
./server_recv
./client_send
# 点对点聊天
./server_chat &
./client_chat
八、总结
| 知识点 | 要点 |
|---|---|
| OSI 模型 | 7 层:物理→数据链路→网络→传输→会话→表示→应用 |
| TCP/IP 模型 | 4 层:网络接口→网络→传输→应用 |
| TCP | 面向连接、可靠、字节流、三次握手 |
| UDP | 无连接、不可靠、数据报 |
| 服务器流程 | socket → bind → listen → accept → read/write → close |
| 客户端流程 | socket → connect → read/write → close |
| 双向通信 | 使用 fork(多进程)或 pthread(多线程)分别负责收和发 |