1. 概念
- 局域网和广域网
- 局域网:局域网将一定区域内的各种计算机、外部设备和数据库连接起来形成计算机通信的私有网络。
- 广域网:又称广域网、外网、公网。是连接不同地区局域网或城域网计算机通信的远程公共网络。
- IP(Internet Protocol):本质是一个整形数,用于表示计算机在网络中的地址。IP协议版本有两个:IPv4和IPv6
- IPv4(Internet Protocol version4):
- 使用一个32位的整形数描述一个IP地址,4个字节,int型
- 也可以使用一个点分十进制字符串描述这个IP地址: 192.168.247.135
- 分成了4份,每份1字节,8bit(char),最大值为 255
- 0.0.0.0 是最小的IP地址
- 255.255.255.255是最大的IP地址
- 按照IPv4协议计算,可以使用的IP地址共有 232 个
- IPv4(Internet Protocol version4):
- IPv6(Internet Protocol version6):
- 使用一个128位的整形数描述一个IP地址,16个字节
- 也可以使用一个字符串描述这个IP地址:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b
- 分成了8份,每份2字节,每一部分以16进制的方式表示
- 按照IPv6协议计算,可以使用的IP地址共有 2128 个
- 查看IP地址
# linux
$ ifconfig
# windows
$ ipconfig
# 测试网络是否畅通
# 主机a: 192.168.1.11
# 当前主机: 192.168.1.12
$ ping 192.168.1.11 # 测试是否可用连接局域网
$ ping www.baidu.com # 测试是否可用连接外网
# 特殊的IP地址: 127.0.0.1 ==> 和本地的IP地址是等价的
# 假设当前电脑没有联网, 就没有IP地址, 又要做网络测试, 可用使用 127.0.0.1 进行本地测试
端口
端口的作用是定位到主机上的某一个进程,通过这个端口进程就可以接受到对应的网络数据了。
Info
比如: 在电脑上运行了微信和QQ, 小明通过客户端给我的的微信发消息, 电脑上的微信就收到了消息, 为什么?
- 运行在电脑上的微信和QQ都绑定了不同的端口
- 通过IP地址可以定位到某一台主机,通过端口就可以定位到主机上的某一个进程
- 通过指定的IP和端口,发送数据的时候对端就能接受到数据了
端口也是一个整形数 unsigned short ,一个16位整形数,有效端口的取值范围是:0 ~ 65535(0 ~ 216-1)
Tip
📌 常见的知名端口号
- HTTP(Web服务器):80HTTP(Web服务器) :80
- HTTPS(加密的Web服务器):443HTTPS(加密的Web服务器) :443
- FTP(文件传输协议):21
- SSH(安全外壳协议):22
- SMTP(邮件发送协议):25
- POP3(邮件接收协议):110
- DNS(域名系统):53
- Telnet(远程登录协议):23
📌 常见的注册端口号
- MySQL数据库:3306
- PostgreSQL数据库:5432
- Microsoft SQL Server:1433微软 SQL 服务器:1433
- Oracle数据库:1521
Question
提问:计算机中所有的进程都需要关联一个端口吗,一个端口可以被重复使用吗?
- 不需要,如果这个进程不需要网络通信,那么这个进程就不需要绑定端口的
- 一个端口只能给某一个进程使用,多个进程不能同时使用同一个端口
OSI/ISO 网络分层模型
OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织组织)在1985年研究的网络互联模型。
Note
- 物理层(驱动):
- 负责最后将信息编码成电流脉冲或其它信号用于网上传输
- 数据链路层(统一接口和上面链接):
- 数据链路层通过物理网络链路供数据传输。
- 规定了0和1的分包形式,确定了网络数据包的形式;
- 网络层(端到端的传输,例如IP层)
- 网络层负责在源和终点之间建立连接;
- 通过IPv4,IPv6格式的IP地址来确定计算机的位置
- 传输层(上面三层是Linux内核,,应用层是应用空间):实现数据分配给任务处理
- 传输层向高层提供可靠的端到端的网络数据流服务。
- 每一个应用程序都会在网卡注册一个端口号,该层就是端口与端口的通信
- 会话层
- 会话层建立、管理和终止表示层与实体之间的通信会话;
- 建立一个连接(自动的手机信息、自动的网络寻址);
- 表示层:
- 对应用层数据编码和转化, 确保以一个系统应用层发送的信息可以被另一个系统应用层识别;
- 应用层:HTTP/HTTPS
TCP/IP常用协议
四层协议:
1.网络接口与物理层:
- MAC地址:48位全球唯一,网络设备的身份标识
- ARP/RAPP:
- ARP:IP--->MAC地址
- RAPP:MAC地址--->IP
- PPP协议:拨号协议(GPRS/3G/4G)
2.网络层(IP层): - IP(Internet Protocol,互联网协议):
- IP 是互联网协议族(TCP/IP 协议栈)的核心协议,负责在不同的主机或设备之间传输数据包。IPv4和IPv6
- ICMP(Internet Control Message Protocol,互联网控制报文协议):
- ICMP 是 IP 协议的辅助协议,主要用于在主机、路由器之间传递控制信息和差错报告。
- IGMP(Internet Group Management Protocol,互联网组管理协议):
- IGMP 是 IP 协议(仅限 IPv4)中的一种协议,专用于管理主机与路由器之间的组播成员关系。
- SCTP(Stream Control Transmission Protocol,流控制传输协议):
- SCTP 是一种面向消息的、可靠的传输层协议,最初设计用于传输电话信令数据(特别是用于 IP 网络上的 SS7 信令)。它结合了 TCP 和 UDP 的优点,并增加了一些高级特性,成为 TCP 和 UDP 之外的第三种主流传输协议。
3.传输层: - TCP(Transmission Control Protocol,传输控制协议) :
- TCP 是一种面向连接、可靠的传输层协议,广泛应用于需要数据准确性和完整性的通信场景。它确保数据包按照顺序、无差错地从发送端传输到接收端。
- UDP(User Datagram Protocol,用户数据报协议):
- UDP 是一种无连接、不可靠的传输层协议,它直接将数据发送给目标主机,不保证数据是否到达、顺序或完整性。相比 TCP,更轻量,传输效率更高
- SCTP(Stream Control Transmission Protocol流控制传输协议):
- SCTP 是一种面向连接、可靠传输的传输层协议,它结合了 TCP 和 UDP 的优点,并增加了多流、多宿主等特性。最初是为了解决传统电话信令(SS7)通过 IP 网络传输的需求,由 IETF(互联网工程任务组)设计并发布
4.应用层: - HTTP 和 HTTPS:
- HTTP,全称是超文本传输协议,用于万维网上的数据传输,比如网页访问。它是一种无状态、基于请求-响应模式的协议,通常通过80端口传输数据。HTTPS 是加密版本的 HTTP,增加了 SSL/TLS 加密机制,常用443端口,确保数据传输的安全性,防止信息泄露和篡改。我们日常浏览网页、访问应用程序接口(API)基本都是通过 HTTP 或 HTTPS 协议。
- FTP:
- FTP 是文件传输协议,用于在不同计算机之间传输文件。它工作在客户端-服务器模式下,通过控制连接和数据连接实现文件的上传和下载。FTP 协议默认使用21端口进行控制,数据传输则可能使用20端口。传统 FTP 明文传输,安全性不高,改进版有 FTPS(加密FTP)和 SFTP(基于SSH的FTP)。
- TELNET 和 SSH :
- TELNET 是一种早期的远程登录协议,允许用户通过终端访问远程设备,但数据传输不加密,安全性较差,常用23端口。SSH(安全外壳协议)则是 TELNET 的加密版,提供加密的远程登录和命令行访问,广泛应用于服务器远程管理,常用22端口。
- NTP :
- NTP 是网络时间协议,用于同步计算机系统之间的时间,确保分布式系统时间统一,避免时钟偏差导致的问题。NTP 常用123端口。
- SNMP(Simple Network Management Protocol,简单网络管理协议):
- SNMP 是一种基于 TCP/IP 的网络设备管理协议,用来监控和管理网络中的设备状态,比如路由器、交换机、服务器、打印机等。它可以帮助网络管理员实时掌握设备运行情况,发现故障并进行远程维护和配置。(实现网络设备集中管理)
- RTP/RTSP:
- 用于传输音视频的协议(安防监控)
Example
OSI模型的工作流程
-
发送方:
- 应用层:应用程序生成数据。
- 表示层:对数据进行格式转换、加密或压缩。
- 会话层:建立会话,管理交互。
- 传输层:将数据分段,添加TCP/UDP头部。
- 网络层:添加IP头部,选择路由。
- 数据链路层:将数据封装成帧,添加MAC地址。
- 物理层:将帧转换为比特流,通过物理介质传输。
-
接收方:
- 物理层:接收比特流。
- 数据链路层:将帧解封装,提取数据。
- 网络层:根据IP地址选择目标主机。
- 传输层:重组数据段,提供可靠传输。
- 会话层:管理会话,确保数据正确传输。
- 表示层:解码数据,解压缩或解密。
- 应用层:将数据传递给应用程序。
2. 网络协议
网络协议指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则的集合。
一般系统网络协议包括五个部分:通信环境,传输服务,词汇表,信息的编码格式,时序、规则和过程。
在网络通信的时候, 程序猿需要负责的应用层数据的处理(最上层)
- 应用层的数据可以使用某些协议进行封装, 也可以不封装
- 程序猿需要调用发送数据的接口函数,将数据发送出去
- 程序猿调用的API做底层数据处理
- 传输层使用传输层协议打包数据
- 网络层使用网络层协议打包数据
- 网络接口层使用网络接口层协议打包数据
- 数据被发送到internet
- 接收端接收到发送端的数据
- 程序猿调用接收数据的函数接收数据
- 调用的API做相关的底层处理:
- 网络接口层拆包 ==> 网络层的包
- 网络层拆包 ==> 网络层的包
- 传输层拆包 ==> 传输层数据
- 如果应用层也使用了协议对数据进行了封装,数据的包的解析需要程序猿做
3. socket编程
Socket套接字由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。(一种特殊的文件描述符)代表着网络编程的一种资源。
Socket套接字目的是将TCP/IP协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用程序能简单地调用该接口通信。这个接口不断完善,最终形成了Socket套接字
Linux系统采用了Socket套接字,因此,Socket接口就被广泛使用,到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h中。
通过上面的描述可以得知,套接字对应程序猿来说就是一套网络通信的接口,使用这套接口就可以完成网络通信。
网络通信的主体主要分为两部分:客户端和服务器端。在客户端和服务器通信的时候需要频繁提到三个概念:IP、端口、通信数据。
3.1 字节序
(访问多字节存在的问题、字符串[单字节]则不存在这个问题)
在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,也就是说对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。
本地字节序-->网络字节序-->本地字节序
目前在各种体系的计算机中通常采用的字节存储机制主要有两种:Big-Endian 和 Little-Endian,下面先从字节序说起。
- Little-Endian -> 主机字节序 (小端)
- 数据的低位字节存储到内存的低地址位, 数据的高位字节存储到内存的高地址位
- 我们使用的PC机,数据的存储默认使用的是小端
- Big-Endian -> 网络字节序 (大端)
- 数据的低位字节存储到内存的高地址位, 数据的高位字节存储到内存的低地址位
- 套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口。
字节序举例
// 有一个16进制的数, 有32位 (int): 0xab5c01ff
// 字节序, 最小的单位: char 字节, int 有4个字节, 需要将其拆分为4份
// 一个字节 unsigned char, 最大值是 255(十进制) ==> ff(16进制)
内存低地址位 内存的高地址位
--------------------------------------------------------------------------->
小端: 0xff 0x01 0x5c 0xab
大端: 0xab 0x5c 0x01 0xff
大小端的使用
不同的系统和处理器架构可能使用小端(Little-Endian)或大端(Big-Endian)字节序。以下是一些常见系统和处理器架构使用的字节序类型:
1. 小端(Little-Endian)系统**:
- Intel x86 和 x86_64 架构:大多数现代个人计算机使用 Intel 和 AMD 的 x86 或 x86_64 处理器,默认采用小端字节序。
- ARM 架构(多数现代设备):现代 ARM 处理器通常默认使用小端字节序,但它也可以支持大端模式,具体取决于硬件配置和操作系统设置。
- 其他:大部分个人计算机、服务器、笔记本电脑、桌面操作系统(如 Windows、Linux 和 macOS)都使用小端字节序。
2. 大端(Big-Endian)系统:
- PowerPC 架构:一些 PowerPC 处理器(例如早期的 Macintosh 计算机和一些嵌入式系统)使用大端字节序,尽管现代 PowerPC 处理器可以在小端和大端之间切换。
- SPARC 架构:一些 SPARC 处理器使用大端字节序,尽管它们也支持小端模式。
- 一些嵌入式系统和网络设备:某些嵌入式设备和网络硬件(如路由器和交换机)通常使用大端字节序。
3. 混合模式:
- ARM架构:如前所述,现代 ARM 处理器可以同时支持小端和大端,允许系统在这两者之间切换,具体取决于应用需求和硬件设计。
总结:
- 小端字节序:大多数现代计算机系统、个人电脑和服务器(例如 Intel x86、x86_64)。
- 大端字节序:一些早期的处理器架构(如 PowerPC),以及某些嵌入式系统和网络设备(如路由器)。
- 可切换字节序的系统:例如 ARM 架构,支持在小端和大端之间切换。
-
函数
BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
#include <arpa/inet.h>
// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int
// 这套api主要用于 网络通信过程中 IP 和 端口 的 转换
// 将一个短整形从主机字节序 -> 网络字节序
uint16_t htons(uint16_t hostshort);
// 将一个整形从主机字节序 -> 网络字节序
uint32_t htonl(uint32_t hostlong);
// 将一个短整形从网络字节序 -> 主机字节序
uint16_t ntohs(uint16_t netshort)
// 将一个整形从网络字节序 -> 主机字节序
uint32_t ntohl(uint32_t netlong);
3.2 IP地址转换
虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换:
主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst);
- 参数:
- af: 地址族(IP地址的家族包括ipv4和ipv6)协议
- AF_INET: ipv4格式的ip地址
- AF_INET6: ipv6格式的ip地址
- src: 传入参数, 对应要转换的点分十进制的ip地址: 192.168.1.100
- dst: 传出参数, 函数调用完成, 转换得到的大端整形IP被写入到这块内存中
- af: 地址族(IP地址的家族包括ipv4和ipv6)协议
- 返回值:成功返回1,失败返回0或者-1
#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 参数:
- af: 地址族协议
- AF_INET: ipv4格式的ip地址
- AF_INET6: ipv6格式的ip地址
- src: 传入参数, 这个指针指向的内存中存储了大端的整形IP地址
- dst: 传出参数, 存储转换得到的小端的点分十进制的IP地址
- size: 修饰dst参数的, 标记dst指向的内存中最多可以存储多少个字节
- af: 地址族协议
- 返回值:
- 成功: 指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串
- 失败: NULL
还有一组函数也能进程IP地址大小端的转换,但是只能处理ipv4的ip地址:
// 点分十进制IP -> 默认是大端整形(内部已经包含了字节序的转化了)
in_addr_t inet_addr (const char *cp);
// 大端整形 -> 点分十进制IP
char* inet_ntoa(struct in_addr in);
3.3 sockaddr 数据结构
// 在写数据的时候不好用 通用地址结构体
struct sockaddr {
sa_family_t sa_family; // 地址族协议, ipv4
char sa_data[14]; // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t; //表示端口号
typedef uint32_t in_addr_t; //表示 IP 地址
typedef unsigned short int sa_family_t; //表示地址族协议的类型
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
//`struct in_addr` 是一个结构体,用于存储 IPv4 地址
struct in_addr
{
in_addr_t s_addr;
};
// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
sa_family_t sin_family; /* 地址族协议: AF_INET */
in_port_t sin_port; /* 端口, 2字节-> 大端 */
struct in_addr sin_addr; /* IP地址, 4字节 -> 大端 */
/* 填充 8字节 */
unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
sin_zero
//在网络编程中,很多系统调用(如 bind
、connect
、accept
等)要求传入的参数类型是 struct sockaddr *
。然而,struct sockaddr
是一个通用的套接字地址结构,它的设计初衷是为了兼容不同的地址族(如 IPv4、IPv6、Unix 域套接字等),所以它的成员比较通用但不够具体。而 struct sockaddr_in
是专门为 IPv4 地址设计的结构体,它包含了更具体的信息(如端口号、IP 地址等)。为了能够在需要 struct sockaddr *
类型参数的系统调用中使用 struct sockaddr_in
结构体,需要保证 struct sockaddr_in
和 struct sockaddr
的大小是相同的,这样才能进行类型转换而不破坏内存布局。sin_zero
数组的作用就是填充 struct sockaddr_in
结构体,使其大小与 struct sockaddr
一致。
3.4 套接字函数
使用套接字通信函数需要包含头文件<arpa/inet.h>,包含了这个头文件<sys/socket.h>就不用在包含了。
// 创建一个套接字
int socket(int domain, int type, int protocol);
- 参数:
- domain: 使用的地址族协议
- AF_INET: 使用IPv4格式的ip地址
- AF_INET6: 使用IPv6格式的ip地址
- type:
- SOCK_STREAM: 使用流式的传输协议TCP
- SOCK_DGRAM: 使用报式(报文)的传输协议UDP
- protocol: 一般写0即可, 使用默认的协议
- SOCK_STREAM: 流式传输默认使用的是tcp
- SOCK_DGRAM: 报式传输默认使用的udp
- domain: 使用的地址族协议
- 返回值:
- 成功: 可用于套接字通信的文件描述符
- 失败: -1
函数的返回值是一个文件描述符,通过这个文件描述符可以操作内核中的某一块内存,网络通信是基于这个文件描述符来完成的。
// 将文件描述符和本地的IP与端口进行绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数:
- sockfd: 监听的文件描述符, 通过socket()调用得到的返回值
- addr: 传入参数, 要绑定的IP和端口信息需要初始化到这个结构体中,IP和端口要转换为网络字节序
- addrlen: 参数addr指向的内存大小, sizeof(struct sockaddr)
- 返回值:成功返回0,失败返回-1
// 给监听的套接字设置监听(阻塞函数)
int listen(int sockfd, int backlog);
参数:
sockfd: 文件描述符, 可以通过调用socket()得到,在监听之前必须要绑定 bind()
backlog: 同时能处理的最大连接要求,最大值为128
返回值:函数调用成功返回0,调用失败返回 -1
// 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 参数:
- sockfd: 监听的文件描述符
- addr: 传出参数, 里边存储了建立连接的客户端的地址信息
- addrlen: 传入传出参数,用于存储addr指向的内存大小
- 返回值:函数调用成功,得到一个文件描述符, 用于和建立连接的这个客户端通信,调用失败返回 -1
这个函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。
// 接收数据
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
- 参数:
- sockfd: 用于通信的文件描述符, accept() 函数的返回值
- buf: 指向一块有效内存, 用于存储接收是数据
- size: 参数buf指向的内存的容量
- flags: 特殊的属性, 一般不使用, 指定为 0
- 返回值:
- 大于0:实际接收的字节数
- 等于0:对方断开了连接
- -1:接收数据失败了
如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞等待数据到达,数据到达后函数解除阻塞,开始接收数据,当发送端断开连接,接收端无法接收到任何数据,但是这时候就不会阻塞了,函数直接返回0。
// 发送数据的函数
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
- 参数:
- fd: 通信的文件描述符, accept() 函数的返回值
- buf: 传入参数, 要发送的字符串
- len: 要发送的字符串的长度
- flags: 特殊的属性, 一般不使用, 指定为 0
- 返回值:
- 大于0:实际发送的字节数,和参数len是相等的
- -1:发送数据失败了
// 成功连接服务器之后, 客户端会自动随机绑定一个端口
// 服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数:
- sockfd: 通信的文件描述符, 通过调用socket()函数就得到了
- addr: 存储了要连接的服务器端的地址信息: iP 和 端口,这个IP和端口也需要转换为大端然后再赋值
- addrlen: addr指针指向的内存的大小 sizeof(struct sockaddr)
- 返回值:连接成功返回0,连接失败返回-1
4. TCP通信流程
**TCP是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。
- 面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。
- 安全:tcp通信过程中,会对发送的每一数据包都会进行校验, 如果发现数据丢失, 会自动重传
- 流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致
4.1 服务器端通信流程
1.创建用于监听的套接字, 这个套接字是一个文件描述符
int lfd = socket();
2.将得到的监听的文件描述符和本地的IP 端口进行绑定
bind();
3.设置监听(成功之后开始监听, 监听的是客户端的连接)
listen();
4.等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的),没有新连接请求就阻塞
int cfd = accept();
5.通信,读写操作默认都是阻塞的
// 接收数据
read(); / recv();
// 发送数据
write(); / send();
6.断开连接, 关闭套接字
close();
在tcp的服务器端, 有两类文件描述符
- 监听的文件描述符
- 只需要有一个
- 不负责和客户端通信, 负责检测客户端的连接请求, 检测到之后调用accept就可以建立新的连接
- 通信的文件描述符
- 负责和建立连接的客户端通信
- 如果有N个客户端和服务器建立了新的连接, 通信的文件描述符就有N个,每个客户端和服务器都对应一个通信的文件描述符
- 文件描述符对应的内存结构:
- 一个文件文件描述符对应两块内存, 一块内存是读缓冲区, 一块内存是写缓冲区
- 读数据: 通过文件描述符将内存中的数据读出, 这块内存称之为读缓冲区
- 写数据: 通过文件描述符将数据写入到某块内存中, 这块内存称之为写缓冲区
- 监听的文件描述符:
- 客户端的连接请求会发送到服务器端监听的文件描述符的读缓冲区中
- 读缓冲区中有数据, 说明有新的客户端连接
- 调用accept()函数, 这个函数会检测监听文件描述符的读缓冲区
- 检测不到数据, 该函数阻塞
- 如果检测到数据, 解除阻塞, 新的连接建立
- 通信的文件描述符:
- 客户端和服务器端都有通信的文件描述符
- 发送数据:调用函数 write() / send(),数据进入到内核中
- 数据并没有被发送出去, 而是将数据写入到了通信的文件描述符对应的写缓冲区中
- 内核检测到通信的文件描述符写缓冲区中有数据, 内核会将数据发送到网络中
- 接收数据: 调用的函数 read() / recv(), 从内核读数据
- 数据如何进入到内核程序猿不需要处理, 数据进入到通信的文件描述符的读缓冲区中
- 数据进入到内核, 必须使用通信的文件描述符, 将数据从读缓冲区中读出即可
服务器端
- 服务器本地绑定 IP 和端口(
bind()
) - 调用
listen()
等待客户端连接 - 使用
accept()
接受客户端连接 - 接收客户端数据 (
read()
) - 处理数据(在你的代码里是直接回显
write()
)
基于tcp的服务器端通信代码:
// server.c
#include <stdio.h>//标准输入输出库头文件
#include <stdlib.h>//标准通用工具库。
#include <unistd.h>//Unix 标准函数库,提供 POSIX 操作系统 API。
#include <string.h>//字符串处理函数库。
#include <arpa/inet.h>//包含与 `IP` 地址转换和套接字地址结构相关的函数和宏。
int main()
{
// 1. 创建监听的套接字
//int socket(int domain, int type, int protocol);
//- `domain`:协议族,如`AF_INET`(IPv4)、`AF_INET6`(IPv6)。
//- `type`:套接字类型,如`SOCK_STREAM`(TCP)、`SOCK_DGRAM`(UDP)。
//- `protocol`:协议编号,通常设为0。
int lfd = socket(AF_INET, SOCK_STREAM, 0);
//创建一个“套接字”对象(Socket),就是程序里的通信端口,用来发送和接收数据。
if(lfd == -1)
{
perror("socket");
exit(0);
}
// 2. 将socket()返回值和本地的IP端口绑定到一起
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000); // 大端端口
// INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
// 这个宏可以代表任意一个IP地址
// 这个宏一般用于本地的绑定操作
//struct in_addr
//{
// in_addr_t s_addr;
//};
// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
//struct sockaddr_in
//{
// sa_family_t sin_family; /* 地址族协议: AF_INET */
// in_port_t sin_port; /* 端口, 2字节-> 大端 */
// struct in_addr sin_addr; /* IP地址, 4字节 -> 大端 */
/* 填充 8字节 */
// unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
// sizeof (in_port_t) - sizeof (struct in_addr)];
//};
//把IP地址转换成网络字序
//方法一:
addr.sin_addr.s_addr = INADDR_ANY; // 这个宏的值为0 == 0.0.0.0
// 方法二: inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//(如 `bind()`、`connect()`、`accept()` 等)需要一个通用的地址结构作为参数
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3. 设置监听//socket套接字fd变成被动套接字[属性转变]
ret = listen(lfd, 128);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 4. 阻塞等待并接受客户端连接
struct sockaddr_in cliaddr;
int clilen = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
if(cfd == -1)
{
perror("accept");
exit(0);
}
// 打印客户端的地址信息
char ip[24] = {0};
printf("客户端的IP地址: %s, 端口: %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(cliaddr.sin_port));
// 5. 和客户端通信
while(1)
{
// 接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
int len = read(cfd, buf, sizeof(buf));
if(len > 0)
{
printf("客户端say: %s\n", buf);
write(cfd, buf, len);
}
else if(len == 0)
{
printf("客户端断开了连接...\n");
break;
}
else
{
perror("read");
break;
}
}
close(cfd);
close(lfd);
return 0;
}
4.2 客户端的通信流程
在单线程的情况下客户端通信的文件描述符有一个, 没有监听的文件描述符
1.创建一个通信的套接字
int cfd = socket();
2.连接服务器, 需要知道服务器绑定的IP和端口以及通信的套接字
connect();
3.通信
// 接收数据
read(); / recv();
// 发送数据
write(); / send();
4.断开连接, 关闭文件描述符(套接字)
close();
5.基于tcp通信的客户端通信代码:
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
// 2. 连接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000); // 大端端口
inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);
//服务器必须先启动,否则 `connect()` 会失败!
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("connect");
exit(0);
}
// 3. 和服务器端通信
int number = 0;
while(1)
{
// 发送数据
char buf[1024];
sprintf(buf, "你好, 服务器...%d\n", number++);
write(fd, buf, strlen(buf)+1);
// 接收数据
memset(buf, 0, sizeof(buf));
int len = read(fd, buf, sizeof(buf));
if(len > 0)
{
printf("服务器say: %s\n", buf);
}
else if(len == 0)
{
printf("服务器断开了连接...\n");
break;
}
else
{
perror("read");
break;
}
sleep(1); // 每隔1s发送一条数据
}
close(fd);
return 0;
}
5. 扩展阅读(Windows平台的套接字)
在Window中也提供了套接字通信的API,这些API函数与Linux平台的API函数几乎相同,以至于很多人认为套接字通信的API函数库只有一套,下面来看一下这些Windows平台的套接字函数:
5.1 初始化套接字环境
使用Windows中的套接字函数需要额外包含对应的头文件以及加载响应的动态库:
// 使用包含的头文件
include <winsock2.h>
// 使用的套接字库
ws2_32.dll
在Windows中使用套接字需要先加载套接字库(套接字环境),最后需要释放套接字资源。
// 初始化Winsock库
// 返回值: 成功返回0,失败返回SOCKET_ERROR。
WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
- 参数:
- wVersionRequested: 使用的Windows Socket的版本, 一般使用的版本是 2.2
- 初始化这个 MAKEWORD(2, 2);参数
- lpWSAData:一个WSADATA结构指针, 这是一个传入参数
- 创建一个 WSADATA 类型的变量, 将地址传递给该函数的第二个参数
- wVersionRequested: 使用的Windows Socket的版本, 一般使用的版本是 2.2
注销Winsock相关库,函数调用成功返回0,失败返回 SOCKET_ERROR。
int WSACleanup (void);
使用举例:
WSAData wsa;
// 初始化套接字库
WSAStartup(MAKEWORD(2, 2), &wsa);
// .......
// 注销Winsock相关库
WSACleanup();
5.2 套接字通信函数
基于Linux的套接字通信流程是最全面的一套通信流程,如果是在某个框架中进行套接字通信,通信流程只会更简单,直接使用window的套接字api进行套接字通信,和Linux平台上的通信流程完全相同。
5.2.1 结构体
///
/// Windows ///
///
typedef struct in_addr {
union {
struct{ unsigned char s_b1,s_b2, s_b3,s_b4;} S_un_b;
struct{ unsigned short s_w1, s_w2;} S_un_w;
unsigned long S_addr; // 存储IP地址
} S_un;
}IN_ADDR;
//`in_addr` 结构体**用于存储一个 IPv4 地址。
//`union`** 是一个联合体,允许多种方式访问同一个内存区域:
//`S_un_b`:将 IP 地址分为 4 个字节(`s_b1`, `s_b2`, `s_b3`, `s_b4`),每个字节是一个 `unsigned char`。
//`S_un_w`:将 IP 地址分为 2 个字(`s_w1`, `s_w2`),每个字是一个`unsigned short`。
//`S_addr`:将整个 IP 地址视为一个 32 位的无符号整数(`unsigned long`)。
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number (端口号)*/
struct in_addr sin_addr; /* Internet address (IP)*/
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
///
Linux
///
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
struct in_addr
{
in_addr_t s_addr;
};
// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
sa_family_t sin_family; /* 地址族协议: AF_INET */
in_port_t sin_port; /* 端口, 2字节-> 大端 */
struct in_addr sin_addr; /* IP地址, 4字节 -> 大端 */
/* 填充 8字节 */
unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
5.2.2 大小端转换函数
// 主机字节序 -> 网络字节序
u_short htons (u_short hostshort );
u_long htonl ( u_long hostlong);
// 网络字节序 -> 主机字节序
u_short ntohs (u_short netshort );
u_long ntohl ( u_long netlong);
// linux函数, window上没有这两个函数
inet_ntop();
inet_pton();
// windows 和 linux 都使用, 只能处理ipv4的ip地址
// 点分十进制IP -> 大端整形
unsigned long inet_addr (const char FAR * cp); // windows
in_addr_t inet_addr (const char *cp); // linux
// 大端整形 -> 点分十进制IP
// window, linux相同
char* inet_ntoa(struct in_addr in);
5.2.3 套接字函数
window的api中套接字对应的类型是 SOCKET 类型, linux中是 int 类型, 本质是一样的
// 创建一个套接字
// 返回值: 成功返回套接字, 失败返回INVALID_SOCKET
SOCKET socket(int af,int type,int protocal);
参数:
- af: 地址族协议
- ipv4: AF_INET (windows/linux)
- PF_INET (windows)
- AF_INET == PF_INET
- type: 和linux一样
- SOCK_STREAM
- SOCK_DGRAM
- protocal: 一般写0 即可
- 在windows上的另一种写法
- IPPROTO_TCP, 使用指定的流式协议中的tcp协议
- IPPROTO_UDP, 使用指定的报式协议中的udp协议
// 关键字: FAR NEAR, 这两个关键字在32/64位机上是没有意义的, 指定的内存的寻址方式
// 套接字绑定本地IP和端口
// 返回值: 成功返回0,失败返回SOCKET_ERROR
int bind(SOCKET s,const struct sockaddr FAR* name, int namelen);
//`Winsock API` 等 Windows 头文件保留 `FAR`,为了与旧的 16 位 Windows 代码兼容
//设置监听
// 返回值: 成功返回0,失败返回SOCKET_ERROR
int listen(SOCKET s,int backlog );
// 等待并接受客户端连接
//`backlog` 指定请求队列的最大长度
// 返回值: 成功返回用于的套接字,失败返回INVALID_SOCKET。
SOCKET accept ( SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen );
// 连接服务器
// 返回值: 成功返回0,失败返回SOCKET_ERROR
int connect (SOCKET s,const struct sockaddr FAR* name,int namelen );
// 在Qt中connect用户信号槽的连接, 如果要使用windows api 中的 connect 需要在函数名前加::
::connect(sock, (struct sockaddr*)&addr, sizeof(addr));
// 接收数据
//char FAR* buf:指向字符数组(或字符串)的指针,用于存储接收到的数据或要发送的数据。
// 返回值: 成功时返回接收的字节数,收到EOF时为0,失败时返回SOCKET_ERROR。
// ==0 代表对方已经断开了连接
int recv (SOCKET s,char FAR* buf,int len,int flags);
// 发送数据
// 返回值: 成功返回传输字节数,失败返回SOCKET_ERROR。
int send (SOCKET s,const char FAR * buf, int len,int flags);
// 关闭套接字
// 返回值: 成功返回0,失败返回SOCKET_ERROR
int closesocket (SOCKET s); // 在linux中使用的函数是: int close(int fd);
//----------------------- udp 通信函数 -------------------------
//UDP(用户数据报协议,User Datagram Protocol)是一种无连接的网络协议,常用于需要快速传输数据但不需要建立连接的应用中。UDP 主要通过两个函数来进行通信:`recvfrom()` 和 `sendto()`。
// 接收数据
int recvfrom(SOCKET s,char FAR *buf,int len,int flags,
struct sockaddr FAR *from,int FAR *fromlen);
// 发送数据
int sendto(SOCKET s,const char FAR *buf,int len,int flags,
const struct sockaddr FAR *to,int tolen);
Info
1. recv()
函数中的 flags
recv()
用于从套接字接收数据,flags
参数控制接收操作的不同行为。
常见 flags
标志:
0
:0
:- 默认行为,没有特殊选项。函数会按照正常的阻塞方式接收数据。
MSG_PEEK
:MSG_PEEK
:- 查看接收缓冲区中的数据,但不会从缓冲区中删除它。
- 使用这个标志可以“窥视”数据而不改变缓冲区的内容。
MSG_OOB
:MSG_OOB
:- 接收带外数据(Out-of-Band Data)。这种数据是与常规数据流分开的,例如网络协议中的紧急数据。
MSG_DONTWAIT
:MSG_DONTWAIT
:- 非阻塞接收。如果没有数据可接收,函数不会阻塞并等待,而是直接返回。
- 通常用于非阻塞模式下,避免长时间等待。
MSG_WAITALL
:MSG_WAITALL
:- 确保接收到指定数量的数据,只有当接收到足够的数据时,函数才会返回。
- 如果请求接收 100 字节数据,
MSG_WAITALL
会确保接收到 100 字节才返回,可能会导致阻塞。
**2. send()
函数中的 `flags
send()
用于向套接字发送数据,flags
参数控制发送操作的行为。
常见 flags
标志:
0
:0
:- 默认行为,没有特殊选项。函数会按照正常的方式发送数据。
MSG_MORE
:MSG_MORE
:- 发送的数据不是一个完整的数据包,表示后续还有更多数据要发送。通常用于 TCP 传输,减少协议头的开销。
MSG_NOSIGNAL
:MSG_NOSIGNAL
:- 发送数据时不发送信号。如果发生错误,系统不会向进程发送信号。
- 用于避免发送错误时进程异常退出。
MSG_DONTROUTE
:MSG_DONTROUTE
:- 数据包不经过路由器直接发送。通常用于本地通信或特定的网络配置。