day027OSI七层协议;tcp三次握手,四次挥手;tcp与udp的区别及两者的撰写方式

本节内容:

1、OSI七层协议
2、tcp三次握手(连接),四次挥手(断开)
3、Tcp和udp的区别
4、Tcp和udp协议下socket撰写方式
5、缓冲区

一、OSI七层协议

1、互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层

下面这些协议简单知道一些常用的就行。(不是重点)

3、各层功能及协议的简单解释

协议就是标准, OSI七层协议就是大家都认可的协议

应用层:是最靠近永华的OSI层,这一层是为了用户的应用程序(例如电子邮件、文件传输和终端仿真)
    提供网络服务

表示层:可确保一个系统的应用层所发送的消息可以被另外一个系统的应用层读取
    如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换

会话层:通过传输层(端口号:传输端口和接收端口)建立数据传输的通路,
    主要在你的系统之间发起会话或者接收会话请求
    (设备之间需要相互认识可以是IP,也可以是MAC或者是主机名)

传输层:定义了一些传输数据的协议和端口号(WWW端口80deng)
    主要是讲从下层接收的数据进行分段和传输,到达目的地址后再进行重组,
    常常把这一层的数据称为段

网络层:在位于不同地理位置的网络中的两个主机系统之间提供链接和路径选择,
    网络层正式管理这种连接的层

数据链路层:定义了以何种格式化数据来进行传输,以及如何让物理介质的访问,这一层通常
    还提供错误检测和纠正,以确保数据的可靠传输。

物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。
    它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后转化为1/0,也就是我们常说的
    数模转换与模数转换)

二、TCP协议的三次握手和四次挥手(重点)

TCP协议是在传输层

1、三次握手(建立连接关系)

1.TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;

2.TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,
同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,
SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。(第一次握手: 客户端 --》 服务端)

3.TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,
同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。
这个报文也不能携带数据,但是同样要消耗一个序号。(第二次握手:服务端 --》 客户端)

4.TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,
此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,
但是如果不携带数据则不消耗序号。(第三次握手: 客户端 --》 服务端)

5.当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。

2、为什么TCP客户端最后还要发送一次确认呢?

本来可以用两次握手搞定的,为什么用三次握手
原因:
一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。

如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,
只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,
此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。
此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,
两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,
但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

3、四次挥手:

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,
然后客户端主动关闭,服务器被动关闭。
服务端也可以主动关闭,流程操作相同,只是换了顺序。
1.客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,
FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),
此时,客户端进入FIN-WAIT-1(终止等待1)状态。
TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。(第一次挥手: 客户端 --》 服务端)

2.服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,
此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。(第二次挥手: 服务端 --》 客户端)

TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,
即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。

这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

3.客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,
等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

4.服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,
由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,
此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。(第三次挥手: 服务端 --》 客户端)

5.客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,
此时,客户端就进入了TIME-WAIT(时间等待)状态。
注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,
当客户端撤销相应的TCB后,才进入CLOSED状态。(第四次挥手:客户端 --》 服务端)

6.服务器只要收到了客户端发出的确认,立即进入CLOSED状态。
同样,撤销TCB后,就结束了这次的TCP连接。
可以看到,服务器结束TCP连接的时间要比客户端早一些。

4、为什么客户端最后还要等待2MSL?

MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。

第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,
站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,
应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,
而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。

第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。
客户端发送完最后一个确认报文后,在这个2MSL时间中,
就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。
这样新的连接中不会出现旧连接的请求报文。

5、为什么建立连接是三次握手,关闭连接确是四次挥手呢?

1、建立连接的时候:

服务器在LISTEN状态下,收到建立连接请求的SYN报文后,
把ACK和SYN放在一个报文里发送给客户端。

2、而关闭连接时:

服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,
而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,
也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,
因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

6、如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。
服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,
若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。
若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

7、三次握手,四次挥手(画图示意)

三、Tcp和udp的区别

记住一点,用socket进行通信,必须是一收一发对应好。
Tcp:面向连接的,消息可靠,效率相对低,消息格式是面向流的

Udp:面向无连接的,消息不可靠,传输效率高,面向包的

1、TCP和UDP对比

TCP(Transmission Control Protocol)
可靠的、面向连接的协议(eg:打电话)、传输效率低全双工通信(发送缓存&接收缓存)、
面向字节流。使用TCP的应用:Web浏览器;文件传输程序。

UDP(User Datagram Protocol)
不可靠的、无连接的服务,传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向报文(数据包),
尽最大努力服务,无拥塞控制。
使用UDP的应用:域名系统 (DNS);视频流;IP语音(VoIP)。

2、直接看图对比其中差异

3、TCP和UDP下socket差异对比图:

4、TCP协议下的socket

基于TCP的socket通讯流程图片:

5、TCP的socket通讯流程(下面一段内容)

先从服务器端说起。
1、服务器端先初始化Socket,
2、然后与端口绑定(bind),
3、对端口进行监听(listen),
4、调用accept阻塞,等待客户端连接。

在这时如果有个客户端初始化一个Socket,
然后连接服务器(connect),
如果连接成功,这时客户端与服务器端的连接就建立了。
客户端发送数据请求,服务器端接收请求并处理请求,
然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束

6、上代码吧,好好解释

server端代码示例(如果比喻成打电话)

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898))  #把地址绑定到套接字
sk.listen()          #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024)  #接收客户端信息
print(ret)       #打印客户端信息
conn.send(b'hi')        #向客户端发送信息
conn.close()       #关闭客户端套接字
sk.close()        #关闭服务器套接字(可选)

tcp_server.py

client端代码示例

import socket
sk = socket.socket()           # 创建客户套接字
sk.connect(('127.0.0.1',8898))    # 尝试连接服务器
sk.send(b'hello!')
ret = sk.recv(1024)         # 对话(发送/接收)
print(ret)
sk.close()            # 关闭客户套接字

tcp_client.py

记住一点,用socket进行通信,必须是一收一发对应好。

7、在看UDP协议下的socket之前,我们还需要知道一些内容:看代码

server端

import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 
sk.bind(('127.0.0.1',8090))
sk.listen()
conn,addr = sk.accept()  #在这阻塞,等待客户端过来连接
while True:
    ret = conn.recv(1024)  #接收消息  在这还是要阻塞,等待收消息
    ret = ret.decode('utf-8')  #字节类型转换为字符串中文
    print(ret)
    if ret == 'bye':        #如果接到的消息为bye,退出
        break
    msg = input('服务端>>')  #服务端发消息
    conn.send(msg.encode('utf-8'))
    if msg == 'bye':
        break

conn.close()
sk.close()

只能与第一个客户端通信server端代码

client端

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090)) #连接服务端

while True:
    msg = input('客户端>>>')  #input阻塞,等待输入内容
    sk.send(msg.encode('utf-8'))
    if msg == 'bye':
        break
    ret = sk.recv(1024)
    ret = ret.decode('utf-8')
    print(ret)
    if ret == 'bye':
        break
sk.close()

只能与第一个客户端通信client端代码

8、tcp的socket,什么是优雅的断开呢?

tcp属于长连接,长连接就是一直占用着这个链接,这个连接的端口被占用了,
第二个客户端过来连接的时候,他是可以连接的,但是处于一个占线的状态,
就只能等着去跟服务端建立连接,除非一个客户端断开了
(优雅的断开可以,如果是强制断开就会报错,因为服务端的程序还在第一个循环里面),
然后就可以进行和服务端的通信了。
什么是优雅的断开呢?看代码。

server端代码:

import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #允许地址重用,这个东西都说能解决问题,我非常不建议大家这么做,容易出问题
sk.bind(('127.0.0.1',8090))
sk.listen()
# 第二步演示,再加一层while循环
while True:    #下面的代码全部缩进进去,也就是循环建立连接,但是不管怎么聊,只能和一个聊,也就是另外一个优雅的断了之后才能和另外一个聊
                #它不能同时和好多人聊,还是长连接的原因,一直占用着这个端口的连接,udp是可以的,然后我们学习udp
    conn,addr = sk.accept()  #在这阻塞,等待客户端过来连接
    while True:
        ret = conn.recv(1024)  #接收消息  在这还是要阻塞,等待收消息
        ret = ret.decode('utf-8')  #字节类型转换为字符串中文
        print(ret)
        if ret == 'bye':        #如果接到的消息为bye,退出
            break
        msg = input('服务端>>')  #服务端发消息
        conn.send(msg.encode('utf-8'))
        if msg == 'bye':
            break
    conn.close()

优雅的断开一个client端之后另一个client端就可以通信的代码

client端代码:

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090)) #连接服务端

while True:
    msg = input('客户端>>>')  #input阻塞,等待输入内容
    sk.send(msg.encode('utf-8'))
    if msg == 'bye':
        break
    ret = sk.recv(1024)
    ret = ret.decode('utf-8')
    print(ret)
    if ret == 'bye':
        break
# sk.close()

client端代码

强制断开连接之后的报错信息:

9、UDP协议下的socket

老样子!先上图!

基于UDP的socket通讯流程:

10、UDP下的socket通讯流程

先从服务器端说起。
1、服务器端先初始化Socket,
2、然后与端口绑定(bind),
3、recvform接收消息,
这个消息有两项,消息内容和对方客户端的地址,
然后回复消息时也要带着你收到的这个客户端的地址,
发送回去,最后关闭连接,一次交互结束

1、基本的代码

server端代码示例

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)   #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9000))        #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr)                 # 对话(接收与发送)
udp_sk.close()                         # 关闭服务器套接字

udp_server.py

client端代码示例

import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)

2、类似于qq聊天的代码示例:

server端代码示例

import socket
ip_port=('127.0.0.1',8081)
udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #DGRAM:datagram 数据报文的意思,象征着UDP协议的通信方式
udp_server_sock.bind(ip_port)#你对外提供服务的端口就是这一个,所有的客户端都是通过这个端口和你进行通信的

while True:
    qq_msg,addr=udp_server_sock.recvfrom(1024)# 阻塞状态,等待接收消息
    print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8')))
    back_msg=input('回复消息: ').strip()

    udp_server_sock.sendto(back_msg.encode('utf-8'),addr)

client端代码示例

#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

qq_name_dic={
    'taibai':('127.0.0.1',8081),
    'Jedan':('127.0.0.1',8081),
    'Jack':('127.0.0.1',8081),
    'John':('127.0.0.1',8081),
}


while True:
    qq_name=input('请选择聊天对象: ').strip()
    while True:
        msg=input('请输入消息,回车发送,输入q结束和他的聊天: ').strip()
        if msg == 'q':break
        if not msg or not qq_name or qq_name not in qq_name_dic:continue
        udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])# 必须带着自己的地址,这就是UDP不一样的地方,不需要建立连接,但是要带着自己的地址给服务端,否则服务端无法判断是谁给我发的消息,并且不知道该把消息回复到什么地方,因为我们之间没有建立连接通道

        back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)# 同样也是阻塞状态,等待接收消息
        print('来自[%s:%s]的一条消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))

udp_client_socket.close()

11、接下来说一个真实的例子,也就是实际当中应用的

就是我们电脑系统上的时间,windows系统的时间是和微软的时间服务器上的时间同步的,
而mac本是和苹果服务商的时间服务器同步的,这是怎么做的呢,
首先他们的时间服务器上的时间是和国家同步的,你们用我的系统,
那么你们的时间只要和我时间服务器上的时间同步就行了,对吧,
我时间服务器是不是提供服务的啊,相当于一个服务端,
我们的电脑就相当于客户端,就是通过UDP来搞的。

我们自制一个时间服务器的代码示例:

server端代码示例

from socket import *
from time import strftime
import time
ip_port = ('127.0.0.1', 9000)
bufsize = 1024

tcp_server = socket(AF_INET, SOCK_DGRAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_server.bind(ip_port)

while True:
    msg, addr = tcp_server.recvfrom(bufsize)
    print('===>', msg)
    stru_time = time.localtime()  #当前的结构化时间
    if not msg:
        time_fmt = '%Y-%m-%d %X'
    else:
        time_fmt = msg.decode('utf-8')
    back_msg = strftime(time_fmt,stru_time)
    print(back_msg,type(back_msg))
    tcp_server.sendto(back_msg.encode('utf-8'), addr)

tcp_server.close()

client端代码示例

from socket import *
ip_port=('127.0.0.1',9000)
bufsize=1024

tcp_client=socket(AF_INET,SOCK_DGRAM)

while True:
    msg=input('请输入时间格式(例%Y %m %d)>>: ').strip()
    tcp_client.sendto(msg.encode('utf-8'),ip_port)

    data=tcp_client.recv(bufsize)
    print('当前日期:',str(data,encoding='utf-8'))

12、两个简易描述socket各个参数和方法的图

1、socket类型:

2、socket各个方法的解释:


四、缓冲区

1.每个套接口都有一个发送缓冲区和一个接收缓冲区。
接收缓冲区被TCP和UDP用来将接收到的数据一直保存到由应用进程来读。 
TCP:TCP通告另一端的窗口大小。 
TCP套接口接收缓冲区不可能溢出,因为对方不允许发出超过所通告窗口大小的数据。
这就是TCP的流量控制,如果对方无视窗口大小而发出了超过窗口大小的数据,
则接收方TCP将丢弃它。 

UDP:当接收到的数据报装不进套接口接收缓冲区时,此数据报就被丢弃。
UDP是没有流量控制的;快的发送者可以很容易地就淹没慢的接收者,导致接收方的 UDP丢弃数据报。

        2.我们经常听说tcp协议的三次握手,但三次握手到底是什么,其细节是什么,为什么要这么做呢?
        第一次:客户端发送连接请求给服务器,服务器接收;
        第二次:服务器返回给客户端一个确认码,附带一个从服务器到客户端的连接请求,客户机接收,确认客户端到服务器的连接.
        第三次:客户机返回服务器上次发送请求的确认码,服务器接收,确认服务器到客户端的连接.
        我们可以看到:
        1. tcp的每个连接都需要确认.
        2. 客户端到服务器和服务器到客户端的连接是独立的.
        我们再想想tcp协议的特点:连接的,可靠的,全双工的,实际上tcp的三次握手正是为了保证这些特性的实现.

结合原理说明
posted @ 2018-11-22 20:42  一片疏影  阅读(210)  评论(0编辑  收藏  举报