1.理解源 IP 地址和目的 IP 地址
- IP 在网络中,用来标识主机的唯一性
- 注意:后面我们会讲 IP的分类,后面会详细阐述IP的特点但是这里要思考一个问题:数据传输到主机是目的吗?不是的。因为数据是给人用的。比如:聊天是人在聊天,下载是人在下载,浏览网页是人在浏览?
但是人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?通过启动的 qq,迅雷,浏览器。
而启动的 qq,迅雷,浏览器都是进程。换句话说,进程是人在系统中的代表,只要把数据给进程,人就相当于就拿到了数据。
- 所以:数据传输到主机不是目的,而是手段。到达主机内部,在交给主机内的进程才是目的。
但是系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程?这就要在网络的背景下,在系统中,标识主机的唯一性。
在进行网络通信的时候,是不是我们的两台机器在进行通信呢?
- 网络协议中的下三层,主要解决的是,数据安全可靠的送到远端机器
- 用户使用应用层软件,完成数据发送和接收的
- 先把这个软件启动起来-->进程
日常网络通信的本质:就是进程间通信!!(从应用层上看,不用看网络栈的底层传输)
要进程间通信,就要先把进程标识出来
2.认识端口号
定义:
- 端口号是传输层协议的一部分。
特点:
- 端口号是一个 2 字节(16 位)的整数
- 用于标识一个进程,告诉操作系统当前的数据应交给哪个进程处理
- IP 地址 + 端口号 可以唯一标识网络上某台主机的某个进程
- 一个端口号只能被一个进程占用
端口号 port: 无论对于 client 和 server,都能唯一的标识该主机上的一个网络应用层的进程
端口号范围划分0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的.
- 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的
3.理解端口号和进程 ID
在公网上:
- IP 地址能标识唯一的一台主机,端口号,能表示主机上唯一的一个进程
- IP:port == 标识全网唯一的一个进程
这种 IP+port 的模式,就叫做 socket (插口,插座) ,实现了客户端和服务器的唯二进程进行通信
- 进程 ID 属于系统概念,技术上也具有唯一性,确实可以用来标识唯一的一个进程,但是这样做,会让系统进程管理和网络强耦合,实际设计的时候,并没有选择这样做
端口号 vs pid
pid 已经能标识一台主机上的进程唯一性了,为什么还要搞一个端口号??
- 不是所有的进程都要网络通信,但是所有进程都要有 pid
- 为什么不直接用 Pid 做端口?将系统和网络部分解耦合,给网络部分设计了单独的规则,防止系统修改进程 pid 造成混乱
就像一个人有身份证号还有工号,不能用工号代替身份证号,是不同场景下的,如果这样那你从公司离职了呢~
我们的客户端,如何知道服务器的端口号是多少?
每一个服务的端口号必须是众所周知的,精心设计,被客户端知晓的
注意:端口号和进程ID都可以唯一表示一个进程, 但是一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
源端口号和目的端口号:
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号
简单来说就是 “
数据是哪个发的, 最后要发给谁
”
4. 传输层协议 -- TCP / UDP
TCP vs UDP 对比
TCP(Transmission Control Protocol 传输控制协议):
- TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议
- TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。
- 其次,TCP协议是保证可靠的协议(也意味着要做更多的事情),数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法
UDP(User Datagram Protocol 用户数据报协议):
- UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议
- 使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。
- 就像发邮件一样,邮件发出去了并不管
传输层同时存在 tcp 和 udp 是为什么呢?
TCP协议和UDP协议不存在哪个更好的说法?
可靠和不可靠都是中性词,就像化学中的惰性一样
- TCP可靠 意味着 在设计和维护上更复杂,不可靠就会相对做的事少一些更简单。
- 所以这两个协议在各自特定的场景下,发光发热
TCP:银行,支付...(在网络联通的情况下,丢包可找回)
UDP:信息派发,例如直播...
5.网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?一般情况下,基本过程为:发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可
为使网络程序具有可移植性,使同样的C语言代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
#include <arpa/inet.h>
uint32 t htonl(uint32 t hostlong);
uint16 t htons(uint16 t hostshort);
uint32 t ntohl(uint32 t netlong);
uint16 t ntohs(uint16 t netshort);
这些函数名很好记,h
表示host
,n
表示network
,I
表示32位长整数,s
表示16位短整数。例如htol
表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
6.Socket编程常见API
// 创建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);
6.2.sockaddr 结构
我们可以看到上面struct sockaddr *addr出现次数挺多的。实际上在网络上通信的时候套接字种类是比较多的,下面是常见的三种:
- unix 域间套接字编程--同一个机器内
- 原始套接字编程--网络工具
- 网络套接字编程--用户间的网络通信
设计者想将网络接口统一抽象化--参数的类型必须是统一的,底层是一种多态的设计
运用场景:
- 网络套接字:运用于网络跨主机之间通信+本地通信
- unix域间套接字: 本地通信
- 我们现在在使用网络编程通信时是应用层调传输层的接口,而原始套接字:跳过传输层访问其他层协议中的有效数据。主要用于抓包,侦测网络情况
我们现在知道套接字种类很多,它们应用的场景也是不一样的。所以未来要完成这三种通信就需要有三套不同接口,但是思想上用的都是套接字的思想。因此接口设计者不想设计三套接口,只想设计一套接口,可以通过不通的参数,解决所有网络或者其他场景下的通信网络。
由于不同的通信方式(跨网络或本地通信)有不同的地址格式,套接字使用不同的结构体来封装地址信息:
- sockaddr_in:用于跨网络通信(例如通过 IP 和端口号进行通信)。
- sockaddr_un:用于本地通信(通过文件路径进行通信)。
为了解决这些不同地址格式的兼容性问题,套接字提供了一个通用的地址结构体 sockaddr,用于统一处理不同的地址结构
sockaddr、sockaddr_in 和 sockaddr_un 的关系
sockaddr 是一个通用的套接字地址结构,它为所有不同类型的通信方式提供了统一的接口。具体通信时,sockaddr 实际上指向特定的地址结构(如 sockaddr_in 或 sockaddr_un),然后通过强制类型转换来区分是哪种通信方式。
这种设计类似于面向对象编程中的“多态”:sockaddr 可以看作一个“父类”,而 sockaddr_in 和 sockaddr_un 是它的“子类”。在程序中,套接字函数接受 sockaddr* 类型的参数,然后根据具体的通信类型进行处理。
6.3.sockaddr 结构体
struct sockaddr {
__SOCKADDR_COMMON(sa_); /* 公共数据:地址家族和长度 */
char sa_data[14]; /* 地址数据 */
};
6.3sockaddr_in 结构体(IPv4 套接字地址)
struct sockaddr_in {
__SOCKADDR_COMMON(sin_);
in_port_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[sizeof(struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof(in_port_t) -
sizeof(struct in_addr)];
};
6.4.sockaddr_un 结构体(Unix域套接字地址)
struct sockaddr_un {
__SOCKADDR_COMMON(sun_);
char sun_path[108]; /* 文件路径 */
};
6.5.IPV4 和 IPV6 的地址表示
- IPV4 和 IPV6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括16位地址类型, 16位端口号和32位IP地址
- IPV4、IPV6 地址类型分别定义为 常数 AF_INET、AF_INET6。这样只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
- socket API 可以都用 stsockaddr* 类型表示,在使用的时候需要强制转化成sockaddr_in,这样的好处是程序的通用性,可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的 sockaddr 结构体指针做为参数
6.6.in_addr 结构
/* Internet address. */
typedef uint32_t in_addr_t;
struct int_addr
{
in_addr_t s_addr;
};
in_addr 是一个32位的整数,用来表示IPv4的IP地址。通信过程中,IP地址通常是通过字符串格式(如 "192.168.1.1")转换为 in_addr_t 类型的数值来表示。
总的来说
- 通过 sockaddr 结构体,Socket API 实现了网络通信和本地通信的统一接口
- 它的设计理念类似于“多态”,即通过一个通用的接口来处理多种类型的地址格式
7.Socket 接口
7.1.创建 Socket 文件描述符
在 TCP 和 UDP 通信中,首先要创建一个 Socket 文件描述符,它本质上是一个网络文件。其函数原型为:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:打开一个网络通讯端口,返回一个文件描述符,如果失败,返回 -1。
参数:
domain:协议域,如 AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(Unix域套接字)。
type:套接字类型,如 SOCK_STREAM(字节流,TCP)、SOCK_DGRAM(数据报,UDP)。
protocol:协议类别,通常设置为 0,自动推导出对应的协议,如 TCP/UDP。
7.2.绑定 bind 端口号 (服务器)
在服务器端,必须绑定一个 IP 地址和端口号,以便客户端可以与服务器建立通信。bind()
函数用于将套接字与 IP 和端口号绑定:
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
功能:将指定的 IP 和端口号绑定到套接字,使之监听指定地址。
参数:
socket:套接字文件描述符。
address:存储地址和端口号的结构体指针。
address_len:地址结构体的长度。