Linux 网络编程入门(一):从 OSI 模型到 TCP Socket 编程

前言:

本文是 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. 1.不支持不同类型的操作系统
  2. 2.不支持不同类型的计算机之间的通信
  3. 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. 1.面向连接:通信前必须建立逻辑通路
  2. 2.可靠传输:无丢失、无失序、无差错、无重复
  3. 3.字节流传输

TCP 三次握手(建立连接):

复制代码
  客户端                          服务器
    |                               |
    |  ---- 1. 我要通话 (SYN) --->  |   客户端发送连接请求
    |                               |
    |  <-- 2. 好的,知道了 (SYN+ACK) |   服务器确认,可以通信
    |                               |
    |  ---- 3. 嗯,好的 (ACK) --->  |   客户端确认,连接建立
    |                               |

为什么是三次而不是两次或四次?------ 三次握手才能实现可靠的连接

应用场景:

  1. 1.对可靠性要求较高的场合
  2. 2.QQ、微信等软件的登录过程

3.2 UDP 协议

UDP(User Datagram Protocol),类似生活中的广播

三大特点:

  1. 1.无连接 ------ 不需要建立连接
  2. 2.不可靠传输 ------ 不保证数据到达
  3. 3.数据报 ------ 一包一包发送

应用场景:

  1. 1.小数据,实时性要求高(QQ 文本消息、语音)
  2. 2.广播、组播
  3. 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. 1.是一个函数接口 ------ 用来使用网络功能
  2. 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(多线程)分别负责收和发
相关推荐
消失的旧时光-19431 小时前
(KMP-Net进阶)第一篇:AppResult<T>——网络层到底应该 throw,还是返回统一 Result?
网络·ktor·kmp
网安蟹佬霸1 小时前
区块链与智能合约安全实战:从Solidity审计到DeFi漏洞深度剖析
运维·前端·网络·安全·自动化·区块链·智能合约
今夕资源网2 小时前
NapCatQQ:把最难接的 QQ 协议层,变成一组能直接调用的接口
网络·qq·napcat·napcatqq·q协议·qq协议
GuoFeng.Wan2 小时前
深入理解经典蓝牙的channel
linux·服务器·网络
影寂ldy3 小时前
C# WinForm TCP-Socket
网络·tcp/ip·c#
周周哈哈哈3 小时前
网络间通信---TCP
网络
程序猿乐锅3 小时前
从 dsh 源码看「一切皆插件」与 Spring IoC
java·网络·数据库·人工智能·后端·spring
艾醒(AiXing-w)3 小时前
LangChain 1.0 入门(五):提示词工程、partial变量、ChatPromptTemplate、Hub模板库
服务器·网络·langchain
wno7044 小时前
nginx反向代理设置ssl,支持https
运维·服务器·网络