hello,各位读者大大们你们好呀
系列专栏:【Linux的学习】
本篇内容:理解源IP地址和目的IP地址;认识端口号;认识TCP协议;认识UDP协议;网络字节序;守护进程;sockaddr结构;地址转换函数;TCP socket API 详解
⬆⬆⬆⬆上一篇:网络基础1
作者简介:轩情吖,请多多指教(>> •̀֊•́ ) ̖́-
网络通信的本质其实是进程间通信
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
端口号(port)是传输层协议的内容
端口号是一个2字节16位的整数
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
一个端口号只能被一个进程占用
另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
因此A主机先将数据通过OS,把数据发送到目标主机,再在本主机将收到的数据推送给上层的指定进程
网络通信的本质:通过IP+端口号构建进程唯一性,来进行的基于网络的进程间通信
我们基于源IP和源端口号,目标IP和目标端口号的表示进程唯一性的方案称为套接字(socket)通信
对于端口号如何找到对应的进程是通过OS内部的一个哈希表,哈希表中是一个个的task_struct的指针
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识
①传输层协议
②有连接
③可靠传输
④面向字节流
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识
①传输层协议
②无连接
③不可靠传输
④面向数据报
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回
首先认识一下几个标识符
PGID是进程组标识符,以第一个小的pid为标识符
SID是会话,会话关联一个终端文件
TTY:终端
上图为对应的终端文件
我们可以发现会话的id就是bash,bash本身自己的会话也是这个,会话>=进程组>=进程,会话会关联一个终端文件,进程组的组长,都是多个进程中的第一个,每次登录,OS就是给你一个命令行解释器,每一个解释器都会创一个会话
shell中控制进程组的方式:jobs;fg;ctrl+z;bg
其实进程组可能是前台任务也可能是后台任务,如果后台任务提到前台,老的前台任务则无法运行,在会话中只能有一个前台任务在运行,如果登录就是创建一个会话,bash就可以启动我们地进程,就是在当前会话中创建新的前后台任务,一旦我们退出,会销毁会话,可能会影响会话内部的所有任务
因此一般网络服务器,为了不受到用户的登录注销的影响,网络服务器一般以守护进程的方式进行运行
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6, 然而, 各种网络协议的地址格式并不相同。
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.。
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好
处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址
我只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换
字符串转in_addr的函数:
in_addr转字符串的函数:
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区.。这个时候不需要我们手动进行释放。因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果,在APUE中, 明确提出inet_ntoa不是线程安全的函数,在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
应用程序可以像读写文件一样用read/write在网络上收发数据; 如果socket()调用出错则返回-1;
对于IPv4,family参数指定为AF_INET; 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议;protocol参数的介绍从略,指定为0即可
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后 就可以向服务器发起连接;
服务器需要调用bind绑定一个固定的网络地址和端口号; bind()成功返回0,失败返回-1;
bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听
myaddr所描述的地址和端口号;
前面讲过,struct sockaddr*是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结 构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;
- 将整个结构体清零;
- 设置地址类型为AF_INET;
- 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑 定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用 哪个IP 地址;
- 端口要转化成网络序列
listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多 的连接请求就忽略;
listen()成功返回0,失败返回-1;
三次握手完成后, 服务器调用accept()接受连接;
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
addr是一个传出参数,accept()返回时传出客户端的地址和端口号; 如果给addr 参数传NULL,表示不关心客户端的地址;
addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度
以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)
客户端需要调用connect()连接服务器;
connect和bind的参数形式一致, 区别在于bind的参数是自己的地址,
而connect的参数是对方的地址; connect()成功返回0,出错返回-1;
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配
客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动
多个客户端, 就会出现端口号被占用导致不能正确建立连接;
服务器也不是必须调用bind(), 但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦
服务器初始化:
1.调用socket, 创建文件描述符;
2.调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
3. 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
4.调用accecpt, 并阻塞, 等待客户端连接过来;
5.建立连接的过程: 调用socket, 创建文件描述符; 调用connect,向服务器发起连接请求;
connect会发出SYN段并阻塞等待服务器应答; (第一次)
服务器收到客户端的SYN,会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段;(第三次)
这个建立连接的过程, 通常称为 三次握手
数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据;
相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据; 服务器从accept()返回后立刻调 用read(),读socket就像读管道一样, 如果没有数据到达就阻塞等待; 这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答;服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求;客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
此时服务器收到FIN后,会回应一个ACK, 同时read会返回0 (第二次);
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN; (第三次)
客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程,通常称为四次挥手
查看具体TCP实现请点击
查看具体UDP实现请点击
网络编程套接字的知识大概就讲到这里啦,博主后续会继续更新更多Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!