socket模块,通信循环、黏包问题

今日学习内容总结

      昨日我们学习了网络编程,对软件开发结构有了一个明确的了解。并且知道了计算机的生产过程中都必须有的相同功能,OSI七层协议,也可以总结成五层。而今天的主要学习内容就是通过代码,实现客户端与服务器的信息交互。

socket

socket套接字简介

      什么是套接字?套接字就是网络间进行通信的方式的名称,行内人一般都称之为套接字通信。比如说HTTP协议,需要具体的编程去实现,或者现在我们做前后端分离项目的时候需要遵循RESTful协议,那么实现此协议的方法就是RESTful API。那么传输层的两种传输服务分别遵循了TCP、UDP协议,实现这两种协议的方法就是套接字。

      套接字的表示方法:

  1.套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的lP地址后面写上端口号,中间用冒号或逗号隔开。
  2.每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。例如:如果IP地址192.168.0.1,而端口号是23,那么得到套接字就是(192.168.0.1:23)

      套接字工作流程

      1.通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket。

      2.根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:

  1.服务器监听:指服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
  2.客户端请求:指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端接字提出连接请求。
  3.连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求,就会响应客户端套接字的请求,建立一个新的线程,并把服务器端套接字的描述发送给客户端。一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,接收其他客户端套接字的连接请求。

      我们今天的学习内容是要编写一个cs架构的程序,实现数据的交互。由于操作OSI七层是所有cs架构的程序都需要经历的过程,所以有固定的模块。这个模块就是socket模块。

socket模块

      socket模块的基本用法:

服务端

  import socket

  server = socket.socket()  # 创建实例

  server.bind(('127.0.0.1', 8080))  # 绑定监听
  """
  服务端应该具备的特征
      固定的地址
      ...  
  127.0.0.1是计算机的本地回环地址 只有当前计算机本身可以访问
  """
  server.listen(5)  # 监听
  """
  半连接池(暂且忽略 先直接写 后面讲)
  """
  sock, addr = server.accept()  # 获取从客户端发过来的数据  没有数据就原地等待(程序阻塞)
  """
  listen和accept对应TCP三次握手服务端的两个状态
  """
  print(addr)  # 客户端的地址
  data = sock.recv(1024)  # recv就是接收数据 用一个最大字节数来作为参数,如果不确定,使用1024比较好
  print(data.decode('utf8'))  
  sock.send('你好啊'.encode('utf8'))  # send发送数据,用字符串作为参数
  """
  recv和send接收和发送的都是bytes类型的数据
  """
  sock.close()  # 主动关闭连接
  server.close()  # 关机

      这段代码的意思是开启一个socket服务,客户端发送过来消息后。经过服务端的处理后。再返回给客户端,然后断开连接。接下来看客户端的代码。

客户端

  import socket

  client = socket.socket()  # 创建一个socket对象
  client.connect(('127.0.0.1', 8080))  # 根据服务端的地址链接

  client.send(b'hello sweet heart!!!')  # 给服务端发送消息
  data = client.recv(1024)  # 接收服务端回复的消息
  print(data.decode('utf8'))

  client.close()  # 关闭客户端

      客户端的代码的意思是,开启连接,连接到指定端口,用户输入数据发送到服务端,然后接受服务端返回的数据。最后再关闭这个连接。

      这样,我们就简单的实现了一个客户端与服务端的首次交互了,虽然只能交互一次就结束。所以接下来的通信循环就是实现服务端与客户端的连续消息发送了。

通信循环

循环通信的实现

      上面两个文件最后都关闭了连接,我们怎么保持消息的连续发送呢?仅仅是不做关闭就可以了吗?答案是不行。我们怎么实现一次连接,就可以持续发送呢,我们可以在一次连接成功后做一个while true的循环,这样我们就可以持续发送消息了。下面是对代码的进一步改写。

服务端

  import socket

  server = socket.socket()  # 创建实例

  server.bind(('127.0.0.1', 8080))  # 绑定监听
  """
  服务端应该具备的特征
      固定的地址
      ...  
  127.0.0.1是计算机的本地回环地址 只有当前计算机本身可以访问
  """
  server.listen(5)  # 监听
  """
  半连接池(暂且忽略 先直接写 后面讲)
  """
  sock, addr = server.accept()  # 获取从客户端发过来的数据  没有数据就原地等待(程序阻塞)
  """
  listen和accept对应TCP三次握手服务端的两个状态
  """
  print(addr)  # 客户端的地址
  
  while True:
    data = sock.recv(1024)  # recv就是接收数据 用一个最大字节数来作为参数,如果不确定,使用1024比较好
    print(data.decode('utf8'))  
    msg = input('请回复消息>>>:').strip()
    sock.send(msg.encode('utf8'))  # send发送数据,用字符串作为参数
  """
  recv和send接收和发送的都是bytes类型的数据
  """
  sock.close()  # 主动关闭连接
  server.close()  # 关机

客户端

  import socket

  client = socket.socket()  # 创建一个socket对象
  client.connect(('127.0.0.1', 8080))  # 根据服务端的地址链接

  while True:
    msg = input('请输入你需要发送的消息>>>:').strip()
    client.send(msg.encode('utf8'))  # 给服务端发送消息
    data = client.recv(1024)  # 接收服务端回复的消息
    print(data.decode('utf8'))

  client.close()  # 关闭客户端

循环通信的代码优化及连接循环

      优化问题:

  1.发送消息不能为空
  2.反复重启服务端可能会报错
  3.连接循环:
      1.如果是windows 客户端异常退出之后服务端会直接报错
      2.如果是mac或linux 服务端会接收到一个空消息

      优化方式:

  1.统计长度并判断即可
  2.在bind前加  from socket import SOL_SOCKET,SO_REUSEADDR
               server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 
  3.1 用异常处理解决
  3.2用len判断

      目前我们的服务端只能实现一次服务一个人,不能做到同事服务多个 ,学了并发才可以实现。

半连接池

      当服务器在响应了客户端的第一次请求后会进入等待状态,会等客户端发送的ack信息,这时候这个连接就称之为半连接。而半连接池其实就是一个容器,系统会自动将半连接放入这个容器中,可以避免半连接过多而保证资源耗光。所以我们的半连接池可以设置最大等待人数。节约资源,提高效率。

      写法:listen(3) 。参数可以设置最大的半连接数,最大3个.

黏包问题

       TCP作为常用的网络传输协议,数据流解析是网络应用开发人员永远绕不开的一个问题。TCP数据传输是以无边界的数据流传输形式,所谓无边界是指数据发送端发送的字节数,在数据接收端接受时并不一定等于发送的字节数,可能会出现粘包情况。

TCP黏包情况:

      1. 发送端发送了数量比较大的数据,接收端读取数据时候数据分批到达,造成一次发送多次读取;通常网络路由的缓存大小有关系,一个数据段大小超过缓存大小,那么就要拆包发送。

      2. 发送端发送了几次数据,接收端一次性读取了所有数据,造成多次发送一次读取;通常是网络流量优化,把多个小的数据段集满达到一定的数据量,从而减少网络链路中的传输次数。

      问题产生的原因其实是因为recv括号内我们不知道即将要接收的数据到底多大,如果每次接收的数据我们都能够精确的知道它的大小,那么肯定不会出现黏包。

解决黏包问题

      因为困扰我们的核心问题是不知道即将要接收的数据多大,如果能够精准的知道数据量多大,那么黏包问题就自动解决了。所以我们解决的方向就是精确获取数据的大小。

  # 可以使用struct模块
  import struct

  data1 = 'hello world!'
  print(len(data1))  # 12
  res1 = struct.pack('i', len(data1))  # 第一个参数是格式 写i就可以了
  print(len(res1))  # 4
  ret1 = struct.unpack('i', res1)
  print(ret1)  # (12,)


  data2 = 'hello baby baby baby baby baby baby baby baby'
  print(len(data2))  # 45
  res2 = struct.pack('i', len(data2))
  print(len(res2))  # 4
  ret2 = struct.unpack('i', res2)
  print(ret2)  # (45,)

      pack可以将任意长度的数字打包成固定长度,unpack可以将固定长度的数字解包成打包之前数据真实的长度。解决思路:

    1.先将真实数据打包成固定长度的包
    2.将固定长度的包先发给对方
    3.对方接收到包之后再解包获取真实数据长度
    4.接收真实数据长度
posted @ 2022-04-15 21:18  くうはくの白  阅读(146)  评论(0)    收藏  举报