目录
一,认识端口号
1.1 背景
1.2 端口号是什么
1.3 三个问题
二,认识Tcp协议和Udp协议
三,网络字节序
四,socket编程接口
4.1 socket常见API
4.2 sockaddr结构
一,认识端口号
1.1 背景
问题:在进行网络通信的时候,是不是我们的的两台机器在通信呢?
解答:我们把软件下载下来安装好,但是不打开,不耗费流量;只有当我们打开,它加载的时候才会耗流量。所以,网络通信的时候,本质是应用层在通信
- 网络协议的下三层:网络层,传输层,数据链路层,主要解决数据安全可靠地被送到远端机器
- 用户使用应用层软件,完成数据地发送和接收,而软件要进行通信就要先启动起来,而启动一个软件也就是进程创建
- 所以日常我们网络通信的本质:就是“进程间通信”,两个进程间要通信就是让两个进程看到同一份资源,这个资源就是网络,而这个网络再具体一点就是网络协议栈
问题: 当传输层即将把数据交给应用层时,但是此时应用层有很多应用,它咋知道要把数据交给哪个应用呢?
解答:所以上层要和传输层达成一种方案,让数据能够准确地交给上层,这个方案叫做“端口号”
1.2 端口号是什么
端口号:是传输层协议的内容,是一个2字节16位的整数,就是4个数字,它用来标识一个进程,告诉操作系统,当前这个数据从传输层要交给应用层的哪一个应用
- 端口号无论对于客户端还是服务器,都能唯一地标识该主机上的一个网络应用层的进程,这点类似于进程的PID,一个端口号只能被一个进程占用
- 在公网上,ip地址能表示唯一的一台主机,端口号(port),用来标识该主机上的唯一一个进程,所以 ip + port = 标识全网唯一的一个进程。
- 所以客户端和服务器都要有自己的ip和port,这种基于ip + port 的通信方式,我们叫做socket,后面会讲
1.3 三个问题
问题:端口号和我们的进程pid有什么区别,两者似乎都能标识该主机上进程的唯一性,那为什么不用pid要用端口号?
解答:
- 不是所有的进程都需要网络通信,但是所有进程都要有pid --> 告诉我们网络是需要单独设计的
- 实现端口号,是为了实现系统和网络的功能解耦,因为系统可能会变,当两者分开设计后,一方收影响就不会影响对方或者对对方的影响大大降低
问题:服务器和客户端是如何知道对方的端口号的?
解答:
首先是客户端如何知道服务器端口号的:
- 服务器和端口号都是同一家公司开发的,所以要想客户端知道端口号,那么这个端口号必须是众所周知的,精心设计的被客户端知晓的
- 这个一般由开发商做的,安装的时候将端口号或者ip直接内置进去了
然后是服务器如何知道客户端端口号的:
- 每次请求都是客户端主动发起的,所以让服务器知道客户端端口号是比较容易的
问题:传输层是如何根据端口号讲数据准确交给应用层众多进程中的那一个对应进程的?
解答:
- 操作系统会在传输层给我们形成一张哈希表,里面存的都是各个进程PCB的指针
- 首先进程绑定端口号的时候,就根据哈希算法找到对应位置,如果这个位置没有PCB指针,就把该进程的PCB指针存进去,当把PCB指针放进哈希表时,就可以认为该进程绑定了端口号
- 然后客户端的报文到了服务器的传输层时,传输层就拿着报文中的端口号在哈希表里做哈希运算,找到哈希表对应位置的进程PCB指针,进而找到对应进程,完成传输层将数据交给应用层的某个具体进程
- 一个进程可以绑定多个端口号,但是一个端口号只能绑定一个进程,因为哈希表是这样规定的,比如哈希表多个位置可以放同一个指针,但是同一个位置只能放一个指针
二,认识Tcp协议和Udp协议
网络协议栈是贯穿整个体系结构的,在操作系统层,应用层和驱动层都有自己的协议。而离我们普通程序员最近的就是使用系统调用接口实现网络通信了,所以离我们最近的就是传输层,传输层应用最广泛最受欢迎的两种协议就是TCP协议和UDP协议了
TCP协议
Tcp:传输控制协议(Transmission Control Protocol),是一种面向连接的,可靠的,基于字节流的传输层协议。
- 如果两台主机想通过Tcp进行数据通信,那么必须先建立好连接道路,并确保建立成功后才进行数据传输
- 同时,Tcp协议也是保证数据传输可靠的协议,数据在传输过程中如果出现了丢包,乱序等情况,Tcp协议都有对应的解决方法,具体我们后面再讲
UDP协议
UDP:用户数据报协议(User Datagram Protocol),是一种无需建立连接,不可靠的,面向数据报的传输层协议
- 如果两台主机要使用Udp通信,无需建立连接,一方根据IP和端口直接就将数据发送给对方,这也就意味着Udp协议是不可靠的,中途出现丢包,乱序等情况,Udp都不会去处理
问题:Tcp比Udp可靠,那为啥传输层要这两种协议同时存在呢?
解答:
- 其实这里的“可靠”和“不可靠”都是中性词,无褒贬含义,就和化学里的“惰性”一样,只是描述某个东西的物理特征,并不是说这个物理懒之类的
- 保证可靠是需要成本的,而相反,不可靠相反的就是简单,TCP在比如说银行转账,微信支付的时候,底层必须是TCP协议,而UDP通常在直播,信息流视频流做数据大量派发的场景有用
- TCP虽然是可靠传输,但并不是万能的,它是保证在网络连通且链接较强的时候处理一些数据丢失问题,也就意味着Tcp的传输效率是没Udp高的,Tcp会在底层做更多的工作
三,网络字节序
计算机在存储数据时是有大小端的概念的:
- 大端:数据的高字节内容保存在内存的低地址处,低字节保存在内存的高字节处
- 小端:数据的低字节内容保存在内存的低地址处,高字节保存在内存的高字节处
如果编写的程序只在本地机器上运行,那么是不需要考虑大小端转换的问题的;但是到了网络通信时,是两台主机在进行进程间通信了,那么这两台主机采用的存储方式可能不一样,比如大端机器传数据给小端机器,那么小端机器解析出来的数据就和大端机器是不一样的
所以我们解决上面的问题,为此:TCP/IP 协议 规定,网络数据流都要采用大端字节序:
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
- 如果当前发送主机是小端,就需要先将数据转成大端,否则就忽略,直接发送即可。
注意:所有的大小端的转化工作由操作系统来完成,因为该操作属于通信细节,不过也有部分的数据需要我们自行进行处理,比如IP的端口号
同时,为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntonl(uint32_t netlong);
uint16_t ntons(uint16_t netshort);
- h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
四,socket编程接口
4.1 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);
4.2 sockaddr结构
可以发现,上面的每个接口的参数都有一个结构体指针 struct sockaddr* addr,下面详细介绍一下:
套接字有三种:
- 1,域间套接字编程:套接字不仅支持跨网络的进程间通信,也支持本地的进程间通信,用的就是这个
- 2,原始套接字编程:通常用来编写一些网络工具,比如监测,抓包等
- 3,网络套接字编程:重点使用传输层,通过TCP和UDP实现用户间的网络通信
所以最开始的套接字结构体提供了两种:
- sockaddr_un:用于本地
- sockaddr_in:用于跨网络
套接字种类不同,就有不同的应用场景,但是网络接口的设计者不想搞三套,计划将网络接口统一抽象化;而网络接口要想统一,那么接口的参数类型必须一致,所以就设计了sockaddr这个结构体:
- 之后在传参数的时候只要传sockaddr这一个就可以了,在设置参数之前就可以往这个结构体添加字段,这点下一篇简单Udp和程序的代码中会具体表现
- 如上图,在调用socket API 的那些接口时,这些API就可以提取 sockaddr 内部的头16字节进行识别,进而得出我们是要进行网络通信还是本地通信,执行对应的操作,完成接口的统一
- 注意:实际在进行网络通信时,定义的还是 sockaddr_in 这样的结构体,只是在传参的时候将该结构体的地址类型强制转换位 sockaddr* 罢了
问题:为啥不用C语言的万能参数 void* 来代替struct sockaddr* 类型呢?
解答:最简单的原因就是,设计网络接口时,C语言还不支持void*传参,而在后面C语言支持void*之后,也很难改回来了,因为这些接口都是系统接口,而系统接口是上层软件接口的基石,所以系统接口不是想改就改的,所以现在的网络接口依旧保留了sockaddr