网络编程:OSI传输层TCP与UDP协议、应用层简介、socket模块介绍及代码优化、半连接池的概念

一、传输层之TCP与UDP协议

  • TCP与UDP协议都是用来规定通信方式的。比如我们在聊天的时候可以随心所欲的聊,有些时候又需要遵循一些规律进行聊天。
  • 当我们跟关系很好的朋友聊天的时候,百无禁忌,上到国家,下到生活中的琐事都可以聊。这就相当于没有设立规定时的通信。
  • 当我们在上课的时候,只能跟老师和同学沟通学习相关的问题。在写作文的时候需要开头空两格,需要分段。写信时需要用上书信格式。这些都相当于设立了规定后的通信。

ps:不遵循上述协议也可以通信 只不过遵循了更合规合法合理!!!

1.TCP协议(重要)

传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。

当应用程序希望通过 TCP 与另一个应用程序通信时,它会发送一个通信请求。这个请求必须被送到一个确切的地址。在双方“握手”之后,TCP 将在两个应用程序之间建立一个全双工 (full-duplex) 的通信。

这个全双工的通信将占用两个计算机之间的通信线路,直到它被一方或双方关闭为止。

三次握手建链接(白话版)

img

1.三次握手建立连接的过程(白话版)

第一次握手:客户端这边向服务端发送建立连接的请求(SYN seq = x)

第二次握手:服务端接收请求并回应,回应的同时向客户端发送建立连接的请求(这里可以看成同时进行了两步,如果服务端同意建立连接,这时候就会建立起客户端发送数据到服务端的数据传输通道,之后的服务端向客户端发送建立连接的请求是为了建立服务端返回数据到客户端的数据传输通道。SYN seq = y,ACK = x + 1)

第三次握手:客户端向服务端进行回应(ACK = y + 1)这时就建立起了两者的双向数据传输通道

2.TCP协议也称为可靠协议(数据不容易丢失)
造成数据不容易丢失的原因不是因为有双向通道 而是因为有反馈机制
给对方发消息之后会保留一个副本 直到对方回应消息收到了才会删除
否则会在一定的时间内反复发送。

3.洪水攻击
同一时间有大量的客户端请求建立链接就会导致服务端一直处于SYN_RCVD状态

4.服务端如何区分客户端建立链接的请求
当我们在建立连接的时候,客户端会发送上图中的请求seq,这个seq带有一串数字,类似识别号码,服务端端受到请求进行反馈的时候返回的信息就是上图中的ACK,ACK会把seq中的识别号码+1然后返回回来,因为不同的客户端发送seq的时候有不同的识别号码,服务端靠这种方式来区分请求(对请求做唯一标识)。

三次握手专业版

img

刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
进行三次握手:

  • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SENT 状态。

    首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。

  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。

    在确认报文段中SYN=1,ACK=1(确认值(Acknowledgement),为1便是确认连接,为0就是不在连接状态),确认号ack=x+1(确认编号(Acknowledgement Number),即接收到的上一次远端主机传来的seq然后+1,再发送给远端主机),初始序号seq=y。

  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

    确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。

在socket编程中,客户端执行connect()时,将触发三次握手。

四次挥手断连接(白话版)

img

1.四次挥手断开连接的过程(白话版)

第一次挥手:当客户端没有信息需要发送给服务端的时候,客户端会发送断开连接的请求(SYN seq = x+2)

第二次挥手:这时服务端会回应客户端,确认断开客户端传输信息给服务端的数据传输通道(ACK=x+3)

TIME_WAIT状态:当断开客户端传输数据到服务端的数据传输通道后,服务端需要先确认是否还有消息需要发送给客户端,发送完毕、确认无误后才会继续后续操作断开连接

第三次挥手:服务端向客户端发送断开通道连接的请求(seq = y + 1)

第四次挥手:客户端向服务端发送回应然后断开服务端跟客户端进行数据传输的通道(ACK = y + 2)

2.四次不能合并为三次
因为中间需要确认消息是否发完(TIME_WAIT)

三次握手和四次挥手也可以看成是小情侣谈恋爱的过程:
​ 三次握手:表白在一起
​ 四次挥手:决裂要分手

四次挥手专业版

img

建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送端还能接收来自另一端数据的能力。

TCP 连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务端均可主动发起挥手动作。

刚开始双方都处于ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:

  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
    即发出连接释放报文段(FIN=1,序列号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
    即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
    即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就关闭连接了,处于 CLOSED 状态。
    即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

在socket编程中,任何一方执行close()操作即可产生挥手操作。

2.UDP协议

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

早期的QQ使用的是纯生的(没有加任何额外功能)UDP协议,导致很容易出现数据丢失,接收不到。现在的QQ自己添加了很多技术和功能。
使用UDP的原因就是因为很简单:快捷、粗暴,只要指定对方的地址就可以发消息了

3.tcp和udp的对比

  • 上面的TCP协议相当于打电话一样,双方有来有回
  • UDP协议就相当于发短信,信息发出去了,但是不一定有回应,因此也称之为数据报协议、不可靠协议

TCP---传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。
UDP---用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快

二、应用层简介

应用层相当于是程序员自己写的应用程序 里面的协议非常的多
常见的有:HTTP、HTTPS、FTP
ps:后续框架部分再做介绍

三、socket模块

1、简介

如果我们需要编写基于网络进行数据交互的程序 意味着我们需要自己通过代码来控制我们之前所学习的OSI七层(很繁琐 很复杂 类似于我们自己编写操作系统)

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

socket模块类似于操作系统 封装了丑陋复杂的接口提供简单快捷的接口

img

ps:socket也叫套接字

2、基于文件类型的套接字家族

套接字家族的名字:AF_UNIX

unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

3、基于网络类型的套接字家族

套接字家族的名字:AF_INET

(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)

四、socket代码简介

服务端代码

import socket

# 1.产生一个socket对象并指定采用的通信版本和协议(TCP)

server = socket.socket()  # 括号内不写参数 默认就是TCP协议  family=AF_INET基于网络的套接字 type=SOCK_STREAM流式协议即TCP

# 2.绑定一个固定的地址(服务端必备的条件)

server.bind(('127.0.0.1', 8080))  # 127.0.0.1为本地回环地址 只有自己的电脑可以访问,8080为端口号

# 3.设立半连接池(后面会讲)

server.listen(5)

# 4.等待接客

sock, addr = server.accept()  # return sock, addr  三次握手
print(sock, addr)  # sock就是双向通道 addr就是客户端地址

# 5.服务客人

data = sock.recv(1024)  # 接收客户端发送过来的消息 1024字节(基于网络传输数据得是二进制)
print(data.decode('utf8'))  # 因为二进制我们看不懂,所以要解码
sock.send('尊敬的客人 您说什么就是什么 一切按照您的要求来'.encode('utf8'))  # 给客户端发送消息 注意消息必须是bytes类型

# 6.关闭双向通道

sock.close()  # 四次挥手

# 7.关闭服务端

server.close()  # 店倒闭了
客户端代码


import socket

# 1.生成socket对象指定类型和协议

client = socket.socket()

# 2.通过服务端的地址链接服务端

client.connect(('127.0.0.1', 8080))

# 3.直接给服务端发送消息

client.send('大爷有钱 把你们店最好的给我叫出来'.encode('utf8'))

# 4.接收服务端发送过来的消息

data = client.recv(1024)
print(data.decode('utf8'))

# 5.断开与服务端的链接

client.close()

五、socket代码优化

1.聊天内容自定义

针对消息采用input获取

2.让聊天循环起来

将聊天的部分用循环包起来(三次握手的代码开始包起来)

3.用户输入的消息不能为空

本质其实是两边不能都是recv或者send 一定是一方收一方发

4.服务端多次重启可能会报错

Address already in use

主要是mac电脑会报
方式1:改端口号
方式2:

#加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
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()        #关闭服务器套接字(可选)

5.当客户端异常断开的情况下 如何让服务端继续服务其他客人?

windows服务端会直接报错
mac服务端会有一段时间反复接收空消息延迟报错
处理方式:使用异常处理、空消息判断,对上面的情况进行判断并给出解决方案

服务端代码:

import socket
from socket import SOL_SOCKET, SO_REUSEADDR

server = socket.socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8081))
server.listen(5)

while True:  # 链接循环
    sock, addr = server.accept()
    while True:  # 通信循环
        try:
            # 当客户端输入为空的时候通过循环跳过后续代码,防止程序报错
            data = sock.recv(1024)
            if len(data) == 0:
                break
            print(f'来自于客户端{addr}的消息>>>:', data.decode('utf8'))
            msg = input('请输入发送给客户端的消息(不能发空消息)>>>:').strip()
            # 返回的信息设置成自定义
            sock.send(msg.encode('utf8'))
        except BaseException:
            # 当客户端直接退出的时候服务端这边会报错,我们用异常处理让程序跳过这些会报错的代码,继续运行
            break

客户端代码:

import socket

client = socket.socket()
client.connect(('127.0.0.1', 8081))
while True:
    msg = input('请输入您想要发送给服务端的消息>>>:').strip()
    # 把发送的信息设置成自定义
    if len(msg) == 0:
        print('不能发送空消息')
        continue
    client.send(msg.encode('utf8'))
    data = client.recv(1024)
    print('来自于服务端发送过来的消息>>>:', data.decode('utf8'))

六、半连接池的概念

server.listen(5) # 半连接池

当有多个客户端来链接的情况下 我们可以设置等待数量(不考虑并发问题)
假设服务端只有一个人的情况下

比如上面我们半连接池设置成五个,在优化后的代码中,由于用while循环包裹了代码,当我们在运行代码的时候可以这样理解,第一个客户端就会直接跟服务端进行交互,除此之外还有五个客户端可以排队,但是发出去的信息服务端那边暂时不会处理,要等到第一个客户端处理结束断开连接才会根据先后顺序依次往后处理

在测试半连接池的时候 可以不用input获取消息 直接把消息写死即可

posted @ 2022-11-16 20:05  wwwxxx123  阅读(91)  评论(0编辑  收藏  举报