端口号(Port)标识了一个主机上进行通信的不同的应用程序
在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信(可以通过netstat -n查看)
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号:
一个进程是否可以bind多个端口号?
可以
一个端口号是否可以被多个进程bind?
端口号->进程需要确保唯一关系,所以不可以
用来查看网络状态
常用选项:
用来查看服务器的进程id
所有协议都包括包括和有效载荷,学习协议的目的就是学习如何解包和分用
UDP传输的过程类似于寄信:
应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并
例如,发送端发送100次,接收端也必须接收100次
在网络通信中,发送端应用层其实并不是直接将数据发送至网络中,而是将数据层层向下传递;通信所使用的IO接口,其实也不是发送接收接口,而是拷贝接口
UDP的socket既能读, 也能写, 这个概念叫做 全双工
我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部).
然而64K在当今的互联网环境下, 是一个非常小的数字.
如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装
TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制
如何解包?
如何分用?
协议报头中包含目的端口号,找到应用层对应的进程,将数据交付给进程
接收端收到一报文后,如何找到曾经绑定的特定端口的进程?网络协议栈和文件又是什么关系?
系统中有很多场景需要快速定位一个进程,所采取的方式就是将进程和端口放入哈希表中,如此一来通过解包报文便可得到端口,再到表中快速定位进程;找到进程之后,每个进程都有PCB进程管理。通过读取到的socket(文件描述符),PCB指向文件描述符数据,便可找到对应的文件结构体,网络协议栈中传输层将数据拷贝到文件结构体的读写缓存区,依次向上交付,最终到达应用层
为什么网络传输中,会存在不可靠问题?
理解起来很简单,举个栗子:如果两个人面对面交谈,没有什么问题,但是如果两个人相隔很远,进行交谈,对方说话的内容都很难听到何谈交流呢?网络也是如此
不可靠的场景如:丢包,乱序,重复,校验错误等
如何保证报文的可靠性呢?
我们认为一个报文只要收到了应答就能保证此报文的可靠性
例如:
收到了应答,只能确保历史消息对方已经收到;由于是双方通信,一定存在最新的消息,没有被应答
双方在通信的过程中,每一方都需要应答;除了正常的数据报,也包括确认数据报
双方通信过程:
因为数据(报文)到达接收方时,顺序不一定和发送时一致,如果采取任何措施,便会造成数据乱序的结果
TCP数据段采取序列号来标识数据段,因此应答报文中,对应的报头中确认报头;所以确认应答和确认序号确保了接收方已经收到ack序号之前的所有报文(必须是连续的);同时也解释了为什么报头中会存在两组序号(序号,确认序号)
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发
至此TCP报头中还剩余16位窗口大小没有介绍,在这之前先引入一个新的问题:TCP通信时,数据不能发送太快但也不能太慢,必须有合适的速度;但是作为发送方又该如何得知,发送的数据是适合的呢?只能通过对方的反馈从而得知对方的接收缓冲区的剩余大小,不过双方都还没通信,怎么会知道对方缓冲区大小呢?这不就变成了先有鸡还是先有蛋的问题了吗?
因此,在第一次请求时,发送方就会将自己的接收缓冲区大小存放在16位窗口大小中发送给对方,对方亦是如此;并且发送的数据并不会立刻被移除,而是必须在发送缓冲区维持一段时间;由此双方便交换了接收能力,也能达到流量控制
不过, client未收到server发来的确认应答, 也可能是因为ACK丢失
因此server会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.
这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果
发送方是如何判定丢包的呢?
其实真正有没有丢包,发送方也不清楚,只是如果超时了,就会被判定丢包
发送端发送出去的数据,并不会立刻被移除。而是会在发送缓冲区中保存一段时间,以防丢包,再次发送
那么, 如果超时的时间如何确定?
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间
在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接
客户端状态转化:
服务端状态转化:
为什么要三次握手呢?既然是三次握手就表示一次,两次都不可以,为什么呢?
一次握手:sever需要维护好已经建立好的链接,如果只握手一次,可能会遭到SYN洪水;两次握手也是如此
四次握手其实也可以,但是没有必要
三次握手是用最小成本验证全双工通信是否通畅,也可以有效防止单机对服务器进行攻击
三次握手不一定会成功,最担心的是最后一个ACK丢失,不过存在对应的解决措施;链接也是要被管理的(OS),先描述,再组织
客户端状态转化:
服务端状态转化:
客户端在FIN_WAIT_2状态时,所谓的不发数据(用户),指的是不发送用户数据,并不代表底层没有管理报文的交互;断开是双方的事情,需要征得双方同意
主动断开链接的一方,最终状态是TIME_WAIT状态
被动断开链接的一方,两次挥手完成,会进入CLOSED_WAIT状态
如果在服务器中将关闭文件描述符的代码注释掉,观察运行结果
客服端断开之前
客户端断开之后
此时服务器进入了 CLOSE_WAIT 状态, 结合我们四次挥手的流程图, 可以认为四次挥手没有正确完成,因为关闭文件描述符的操作没有实现
对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题
首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server, 结果是
这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监 听同样的server端口
为什么是TIME_WAIT的时间是2MSL?
四次挥手的动作已经完成,但是主动断开链接的一方要维持一段时间的TIME_WAIT,为什么呢?
需要保证最后一个ACK尽可能地被对方收到;双方在断开链接时,网络中可能还有滞留的报文,保证滞留报文进行消散
服务器有时可以立即重启,但是上面这种情况无法进行立即重启,如果在服务器运行繁忙时,此时会非常棘手
在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
再次运行服务器
刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段;这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候;并且如果我们发送数据,没有收到应答之前,必须将已发送的数据暂时保存起来,为了支持超时重传,所保存的地方其实就是发送缓冲区,发送缓冲区和滑动窗口紧密关系
不严格的情况下,发送缓冲区可以分为三部分:
其中已发送&&!收到应答的部分称作滑动窗口
如何看待滑动窗口呢?
可以将其理解为一个字符数组
窗口移动,其本质就是下标在进行更新
目前的理解是滑动窗口的大小和对方的接收能力有关,无论未来怎么滑动,都要保证对方能够进行正常的接收
win_start=ack_seq;win_end=win_start+tcp_win
ack_seq
报文的确认序号 tcp_win
对方的接收缓冲区剩余空间
窗口一定会向右滑动吗?会向左滑动吗?
可能向右滑动,取决于右边界的变化;一定不会向左滑动,因为收到应答的数据不可能变成没有收到应答
窗口一定会一直不变吗?会变大吗?会变小吗?为什么?
窗口一定会变化的;当发送的数据越来越多,对方一次将大量的数据从接收缓冲区中取走,此时窗口就会变大;当对方一直不取走接收缓冲区中数据,窗口便会变小
收到应答的时候,如果不是最左侧发送的报文的确认,而是中间的,或者最右侧的怎么办?需要滑动吗?
两种可能
滑动窗口必须要滑动吗?会不会不动?或者变为0?
不一定滑动,可能不动,也可能变为零
只取决于 win_start=ack_seq;win_end=win_start+tcp_win
两者的差值
如果一直向后滑动,空间不够怎么办?
发送缓冲区是环状结构的,所以空间不存在不够
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
接收端如何把窗口大小告诉发送端呢?
在三次握手时就已经交换了窗口大小
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题
在之前我们考虑的只是通信双方,并没有考虑到两者之间的网络
因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据
像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率
那么所有的包都可以延迟应答么? 肯定也不是
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的. 意味着客户端给服务器说了 “How are you”, 服务器也会给客户端回一个 “Fine, thank you”; 那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起回给客户端
简单理解就是在确认应答时也发送数据
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界
对于UDP协议来说, 是否也存在 “粘包问题” 呢?
进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别
机器重启: 和进程终止的情况相同
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性:
提高性能:
我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较
归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定
举个栗子
在大型商场中,每个饭馆外面都有闲置的座椅,每当到饭点时,都会有顾客在外面排队,仔细观察会发现,这些座椅并不会很多,因为没有必要;让顾客排队的目的就是在里面有资源空闲时,立刻投入使用,提高入座率;在TCP协议中同样如此
TCP协议,要为上层维护一个链接队列,当服务器有空闲的资源时,队列中的客户端立刻进行连接;队列的长度受listen的第二个参数影响
对于服务器, listen 的第二个参数设置为 2, 并且不调用 accept
当对服务器进行三次连接之后,查询结果如下
当进行第四次连接时,查询结果
当第四次连接时,客户端认为自己已经完成握手,但是服务端并没有完成握手,并没有最后一次确认,这种情况也称半连接
Linux内核协议栈为一个tcp连接管理使用两个队列:
全连接队列的长度会受到 listen 第二个参数的影响;全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态
;这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1;同时也说明了accpet与三次握手没有关系