端口号可以识别将数据传输给哪一个应用程序。
在tcp/ip协议中,用源ip,源端口号,目的ip,目的端口号,协议号" 这样一个五元组来标识一个通信(可以通过netstat -n查看);
0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的. 你是不能绑定的。
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.(16字节最大能表示的数)
ssh服务器, 使用22端口
ftp服务器, 使用21端口
telnet服务器, 使用23端口
http服务器, 使用80端口
https服务器, 使用443
SSH是一个可以安全远程登陆的服务器(telnet也可与远程登陆,但是不安全。)
SSH既然可以远程登陆,那么我们是否可以在一个装有ssh的任意系统的主机上登陆云服务器呢?
答案是可以的。
ssh 账号名@ip地址
我就可以在windows上的cmd登陆进来了。
手机上也可以用ssh登陆。
一个进程是否可以有两个端口号?
可以。不过不常见。ftp里面有这种情况。(其实只要能通过端口号找到对应pcb就可以)
一个端口号是否可以被两个进程绑定?
不可以,这样就无法通过这个端口号找到对应的进程了。
netstat是一个用来查看网络状态的重要工具.
语法:netstat [选项]
功能:查看网络状态
常用选项:
带n选项的local address是22
可以直接查看进程的pid
怎么用pidof和kill结合来终止进程?
这是不对的,因为管道相当于把pid写入到匿名管道里面,kill进程从里面读取信息。
最后就相当于 kill pid -9,格式不对。
要把pid作为参数传给kill进程,要用xargs来传
pidof httpServer | xargs kill -9
这是tcp通信的过程,先connect,然后发送request,然后接收respond
最后disconnect
UDP不保证可靠性
第一个是源端口号
第二个是目的端口号
第三个是整个udp数据报的大小,包含了报头和data
第四个是udp的校验和
有几个问题说一下:
UDP传输的过程类似于寄信.
应用层发给udp的数据不能拆分也不能合并(面向数据报)
举个例子:如果上层发了一个100字节的数据,udp不能调用10次recvfrom,每次读10个字节。必须读一次,一次读100字节。
udp length是16字节,因此udp length最大是65536字节,也就是64kb。
DNS:域名解析协议
tcp可以解决可靠性和效率两大方面
为什么会有可靠性问题?
网络传输,线路更长,容易出现问题。
不可靠问题都有哪些?
丢包(ACK+确认序号解决)
乱序(序号解决)
错误(错误了就丢掉了,也是丢包的一种)
接收缓冲区满了,来不及接收了(接收不了了就丢掉了,也是丢包)
4位首部长度表示该TCP头部有多少个32位bit(有多少个4字节);
tcp的header标准长度是20,但是有一个选项,选项的长度取决于该tcp的data offset(4位header长度)
什么意思呢?
当data offset为5的时候,header长度为20,因此选项长度为0
当data offset为15(4个bit最大能表示的数),header长度为15 * 4 = 60,因此选项长度为60 - 20 = 40
注:data offset不能为0.因为首部长度最少也要20字节。
如何保证发送信息被对方接收到了?
答案:通过对方是否给自己的回应来判断对方是否收到了自己的消息。
注:ACK (Acknowledge character)即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。(ACK可以附带数据一起发送,后面讲)
这种机制叫做确认应答机制。
下面这个图很形象的说明了确认应答机制。发一句我ACK一次
这里讲一下http的send和tcp的发送数据的关系:
send和tcp中的发送数据并没有直接关系。
如果server的ACK发送失败了,怎么办?server要不要继续ACK?
答:不用。有一种机制叫重传机制,client端没有接收到server的ACK,他就认为是自己发送数据失败了,重新发送一遍。
其实client是不知道这个数据丢了的,这种行为是人为规定的。
促发超时重传机制有两种可能性:
1.request丢了
2.request成功发送过去了,但是服务端的ACK丢了
在第二种情况:server端是收到了两次同样的数据,server会把重复的数据丢掉。
server怎么知道重复了呢?
序号相同,因此重复了。
超时的时间怎么定呢?
超时的时间是浮动的,根据网络情况而定。网络差时间就久一点,网络好时间就短一点。
linux中超时是以500ms为单位进行操控。
第一次等500ms重发
第二次等2500ms重发
第三次等4500ms重发
第四次等8*500ms重发
直到超过一个阈值,就会自动断开连接。
我们发数据的时候,有可能会乱序。比如说hello world,发送的时候可能先发送world,再发送hello。因为发送距离太远了,由于路径选择问题,信息可能并不会按顺序到达。
32位序号就是防止乱序而出现的问题的。他会给数据编号。
它会给hello world给编个号,给hello编号1,再给world编号2。即使接收的时候是乱序的,最后排一下序即可。
具体编号方式:
tcp为每一个数据的字节都编上了序列号
注:没有数据的tcp header的序号是不会变的,(可以理解成没有)。序号只会给数据编号。
现在有这么一个情况,client发了三次request,server应该回应三次ACK来确定收到了这三个信息。
那我们怎么区分这三个ACK是回应哪一条消息的呢?
因此我们要给ACK编上号。这个确认的应答的编号应该是和对应的request相对应的。(不是一样的)
比如说现在我要发hello, world, !!!,编号分别是6,7,8.
那么我们的ACK的确认序号就是7,8,9。
会在原始header序号基础上+1
举几个例子再解释一下:
(下面这个很重要,也是tcp允许丢包的现象)
总结一下:
确认序号是server给client发的ack的编号,值是原始序号+1.代表的含义是原始序号为确认序号-1的所有数据都被收到了。
简单来说是表示历史上的都收到了,这个是协议规定的。是为了配合滑动窗口才这么设置的。
既然client给server发送request的时候server只关心client的序号,server给client发送ACK的时候client只关心server的确认序号,那为什么一个header里面要同时包含两个序号?
不可以client给server发送的时候只填序号,server给clientACK的时候只填确认序号吗?
答:tcp是全双工的。读写可以同时进行,这也是后面要讲的ACK带数据的情况。ACK带数据的时候,client端既需要ACK的确认序号,又需要数据的序号。
之前看过sock的代码,里面有两个接收和写队列,它就是tcp的接收缓冲区和发送缓冲区
send函数和recv函数本质上是把在用户区的数据拷贝到了内核区的receive_queue和write_queue里面了,其实是一个拷贝接口。
tcp的缓冲区本质上就是一个生产者消费者模型。
指的是发送方的接收缓冲区剩余空间的大小
是发送端的接收缓冲区的剩余空间还是接收端的接收缓冲区的剩余空间呢?
是发送端的!!!为什么呢?
因为别人在发送数据返回给你的时候,大小要合理,不能直接乱发。
可以通过窗口大小来进行流量控制。
总结一下:
我要发送数据给接收方的时候,就必须把我自己的窗口大小,即自己的接收缓冲区剩余空间大小发送给接收方,实现流量控制。
ACK (Acknowledge character)即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。
SYN:同步序列编号(Synchronize Sequence Numbers)。是TCP/IP建立连接时使用的握手信号。在客户机和服务器之间建立正常的TCP网络连接时,客户机首先发出一个SYN消息,服务器使用SYN+ACK应答表示接收到了这个消息,最后客户机再以ACK消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。
FIN:finish,终止
PSH:push,提示接收端应用程序立刻从TCP缓冲区把数据读走
RST:reset。重新建立连接。
下图是发生rst的情况:
URG:紧急指针是否有效,1代表有效,0代表无效。紧急指针指向的数据叫带外数据(out of band)说人话就是加急的数据,需要优先读取。紧急指针只能指向一个字节的数据。在内核里面紧急指针不是指针,只是一个整数,0代表没有,1代表一个字节。
1.链接是什么?
链接本身是有成本的,因为链接是需要创建数据结构来管理的。
tcp3次握手成功之后,client和server都要维护链接。
2.为什么是三次握手?
如果用1&2次握手,容易遭到syn洪水攻击。
什么意思呢?
如果用1次握手,即client发一次syn就建立连接,那client疯狂的发syn就会导致创建很多连接,创建连接是需要成本的,会导致server顶不住了。
如果用2次握手也是同理,client发一次syn,server立刻回一个ack。server在发送完ack之后就建立连接了,client可以最后把ack丢掉,不让client端建立连接,就可以让client没有负担的疯狂的发送syn,server创建很多连接,也会挂掉。(因此偶数次的握手是绝对不可取的,因为是服务器先承担了建立连接的责任。)
三次握手可以挡住基本的洪水攻击,至少在建立链接的时候client端也同样地要维护链接。即使client端发动洪水攻击,它自己的主机上也要先建立很多垃圾连接。
为什么三次握手可以?
1.用最小成本验证双工。(读写同时进行)解释一下:验证全双工的方式就是我既能收到你的信息也可以发送信息给你。如果是1次握手,server只收,client只发,无法验证另外的功能。如果是2次握手,server接收syn后发送ack,但是无法知道client是否收到了自己的ack,因此也无法验证。
2.让服务器不要出现连接建立的误判情况。解释一下:当三次握手最后一次client发送ack的同时,client就认为连接已经建立好了,但是最后一次ack是有可能会丢失的,如果丢失了服务器是不会建立连接的。这样服务器就没有资源损失,损失让client承担了。我们要选择让客户端去承担这个责任,因为server是一对多的,多次遭受误判会导致建立很多垃圾连接,导致资源浪费。
为什么要四次挥手?
client和server是双工通信的,因此断开连接要关闭client的通道和server的通道。关闭之后又要ack,因此是四次。
1.client发送FIN,server发送ACK。
这里有一个问题:client发送断开发送连接,底层的数据结构有被释放吗?以后还会发数据给server吗?
答:第一次发送完FIN之后,底层数据结构并不会被释放。它所说的断开发送连接指的是应用层不再发送数据了,即用户无法在从用户区把数据拷贝到内核里的发送队列了。它之后在server端发送FIN之后还会回应一个ack的。那个时候才会释放底层的数据结构。
2.client发送FIN,在应用层干了什么?
答:client端close(sock)关闭连接。
TIME_WAIT要求主动断开连接的一方(server和client都可以进入TIME_WAIT),要进行等待。为什么?
假设主动断开的一方是客户端。
客户端在最后发送ack的时候有可能会丢失,如果没有TIME_WAIT,客户端发送完ack直接就关闭连接了,万一此时ack丢了,server端收不到ack,他就超时重传FIN,客户端还是收不到,server继续重传,这会浪费资源。
如果有TIME_WAIT,client的ack还是可能丢,server会重传。client收到重传之后继续ack,这样server的连接肯定可以关闭。
如果ack没有丢,server的连接关闭了,TIME_WAIT等了一段时间发现没有FIN重传过来,他就认为连接已经关闭了,它也从TIME_WAIT变成CLOSE了。
总结一下:
1.保证ack被对方收到了。
2.等待历史数据在网络上进行消散。有可能断开连接的请求是先于数据发送的请求的,因此要等待所有数据都发送完才可以断开连接
为了做到上面几点,TIME_WAIT设置成2MSL。 MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长的最长时间,超过这个时间报文将被丢弃。
这个状态代表对象认为自己的连接已经建立好了。比如说三次握手之后,client发出ACK之后立马就进入了established,因为它认为它已经建立好连接了。server收到了ack之后也进入了established。
服务端状态转化:
客户端状态转化:
注意:由于用的是云服务器,因此观察很麻烦。
telnet的时候千万不要在云服务器上面telnet,在windows的cmd上telnet,看的会舒服很多。
抓包命令
-i表示监听接口
any是所有接口
-nn是把能显示成数字的都显示成数字
tcp是抓tcp的包
port8080是抓端口号为8080的包
sudo tcpdump -i any -nn tcp port 8080
client的ip是220.115.159.7,端口号是49481
server的ip是172.21.0.2 端口号是8080
client先发了一个syn给server
server再发了一个syn+ack给client
client再发一个ack给server
netstat -npt看一下,现在连接已经创建起来了,已经是established状态了。(由于主机是服务器,远端是客户端,因此local address是服务器)
要浮现出CLOSE_WAIT的状态,就让server端不要close(sock)就好了。
由于我让服务器不close(sock),因此服务器永远不会进入LAST_ACK状态给client发FIN。
下图我们发现client给server发了一个FIN+ACK,server回了一个ACK
之后就没了。
netstat查看:server确实在CLOSE_WAIT状态
总结:
对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题.
要复现TIME_WAIT状态就让主动断开连接的那一方不要close(sock)即可。由于本地是服务器,因此我们这次先退出服务器。
可以发现出现了TIME_WAIT
这也是为什么我们有时候退出了服务器,短时间再绑定同一个端口号的时候会出现bind error的原因了,因为有连接还没有断开,服务器先关闭导致一直在TIME_WAIT状态出不来了。
那为什么等一段时间就又可以了,之前讲过TIME_WAIT等待的时间是2MSL,过了这个时间就好了。
一个大型服务器如果上面连了几千万个连接,然后忽然服务器挂掉了。服务器成了先退出的一方,服务器现在要重启,重启的时候由于每一个链接都处于TIME_WAIT状态,重启之后服务器也无法bind成功。那服务器就只能等了,怎么解决这个问题呢?
用setsockopt
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
这样就不会出现由于进入了TIME_WAIT而导致的bind error问题了。
它的写法和其他socket函数都差不多,基本是固定的
int opt = 1;
setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt写的位置应该在刚创建出来的socket后面
顺序如下:
socket()
setsockopt()
bind()
listen()
accept()
...
刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.
这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了).
如图:
窗口内的数据代表要发送的数据,当发送之后收到ack,窗口就会往右移动。比如说:上图收到ack的确认序号为36,则证明32-35的数据都收到了,窗口就移动到36了。
**超时重传机制和滑动窗口也有关系,**如果32没有收到ack,窗口就不动了,等待一定时间后再继续发送。
情况1
这些ack丢了没什么关系,因为它们不是那一批当中的最后一个ack。当4001的ack被收到之后,滑动窗口认为之前那一批1 - 4000的所有数据都被收到了,他就开始移动了。
这种情况是数据丢了,1001-2000的数据丢了,没有给ack,因此滑动窗口不会动的。即使收到了除了1001-2000的数据以外的所有数据都收到了,ack回应的依然是1001,它要求发送方从1001开始重新发数据。
协议规定,如果收到连续三个同样的ack,发送方就会立刻重传。这种重传方式叫做快重传。
快重传和超时重传是互相补充的。
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
一旦接收方的接收队列满了。有两个机制可以让发送方知道它什么时候才可以继续发数据。
接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;
那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;
当网络很拥塞的时候,如果发送方发送10000个数据,9999个都丢了,那么发送方不会重传,而会等待。因为本来网络就已经很拥塞了,此时继续重传,会加重网络的压力。
这叫拥塞控制。
但不能一直等吧,拥塞控制采用的算法叫慢启动。
TCP引入了慢启动的机制,先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据
像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快
为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍. 此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
但拥塞窗口也不是一直会变大的,即使网络不拥塞,拥塞窗口的大小还和对方的窗口大小有关系。如果对方的窗口大小很小,拥塞窗口不会一直增大,因为受流量控制的缘故。
总结一下窗口:有滑动窗口和拥塞窗口,这两个窗口是发送方的窗口,还有窗口大小,是接收方的窗口。
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
总结一下就是:延时应答就是收到数据后不立刻ack,等上层得用户层先取走缓冲区得一部分数据之后再ack。
这个策略就是为了提高tcp的传输效率。
那么所有的包都可以延迟应答么? 肯定也不是;每个系统都有自己的规定。
我们知道隔了几个包之后就必须应答一次,或者隔了一段时间就应答一次即可。
应答的时候带数据就是捎带应答。
比如ack带数据就是捎带应答。
捎带应答有可能会让最后的四次挥手变成三次挥手。server收到FIN之后,捎带了上一次对FIN的ack和下一次的FIN进来,本来两次的通信变成一次。
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性:
提高性能:
8. 滑动窗口
9. 快速重传
10. 延迟应答
11. 捎带应答
其他:
定时器,用于为超时重传,TIME_WAIT,这些要计时的东西计时。
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;(之前看过的那个sock结构体里面的两个queue)
和udp对比一下:
面向数据报就不可以这样,给了100个字节只能一次读100个字节,不能拆开。原因就是udp没有缓冲区。而tcp有缓冲区。
之前讲过管道也是面向字节流的:
管道也是面向字节流的。因为管道本身就是一段缓冲区。有100个字节的数据,你可以一次读100,也可以读10次10字节。
之前说过,面向字节流容易出一个问题就是不知道一个数据包的边界在哪里,很容易读出界。这种问题叫粘包问题。
为了解决粘包问题,我们就要分开每一个数据包的边界。这个工作并不是tcp做的,而是应用层做的。比如http每次向下交付数据的时候,会加上自己的报头。http的报头上有空行,可以分开报头和数据。读到空行的时候再读content-length个字节就可以把第一个数据包的正文读完。然后后面的就是第二个数据包了。
总结一下就是:tcp并不关心数据包的边界问题,这个问题是由应用层自己加上边界解决的。
udp有粘包问题吗?
没有,因为udp自己的报头就做好了数据包的边界,直接读就好了。也可以理解成udp是面向数据报的,因此不会有粘包问题。
我们知道发数据和收数据都是进程在做,如果此时我正在发消息,进程崩了,会发生什么?我们之间的链接会怎么样?
我们知道套接字是file*指向的,因此套接字本质也是一个文件。文件的生命周期是随进程的(在进程间通信讲过,管道的生命周期也是随进程的)
因此我们可以得到一个结论:进程终止之后,这个链接也没有了。
但是这个链接是怎么没了的呢?是否要告诉对方我们要断开连接了?
答案是要的,进程退出的时候会直接触发四次挥手,和我们应用层调用close没有区别。
我们关机的时候,有可能会提示xxx正在运行,正在关闭xxx。
所以关机之前要把所有进程终止的,因此机器重启的情况和进程终止的情况是一样的,都是直接触发四次挥手。
接收端认为连接还在,一旦接收端有写入操作,接收端就可以发现连接已经不在了。(因为直接拔网线没有机会四次挥手,对方不会认为连接断开了。)
即使没有写入操作,tcp也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,就会把连接释放。
链接队列的长度。这个队列是存放等待链接的。
accept本质是从连接的等待队列中把链接读取上来。相当于从缓冲区上读数据
如果accept不读取,相当于这个链接就一直在传输层,无法到应用层。
linux里面有两种链接队列。
先讲全链接队列。全链接队列是用来存放established状态的链接的。它最多能存放backlog + 1个established的链接。
半链接队列是用来存放sys_recv的半链接(准确来说不是链接,因为三次握手没有完成)的,当全链接队列放满的时候,就无法再建立established状态的链接了。此时如果客户端还继续连接,发送syn,server只会回一个syn+ack,至于client再次回复的ackserver端就不管了,直接丢弃。
至于半链接队列能放多少个sys_recv状态的半链接,取决于操作系统。
注意:listen的backlog里面的都是创建好的连接。accept只是读取到应用层而已。读不读取和这个连接是否被创建没有关系,只要listen了就是创建了。
为什么实际存放的链接数会比backlog大1?后面讲内核代码的时候讲
在inet_connection_sock里面有一个成员,它是用来管理链接队列的.accept就是从这里拿走request_sock_queue里面的链接的。
因此它的类型也是request_sock_queue
request_sock_queue里面就有两种链接队列。一个用来放request_sock,一个用来放listen_sock。到这里我们有一个感觉了,链接其实就是request_sock.
链接的成员
在sock结构体里面,有这两个成员
很明显,第一个是记录当前request_queue里面的链接数量的,第二个是记录request_queue最大能放多少链接数的。内核是如何判断这个request_queue是满的呢?很特别,这也是为什么+1的原因。
它比较是否满用的是大于符号,不是大于等于。有点奇怪,但事实如此。
内核所有报文都是这个结构体,我们可以看到tcp的缓冲区write_queue和recv_queue里面放的类型全是sk_buff