socket套接字

day33

 

socket套接字简介

socket 其实就是操作系统提供给程序员操作「网络协议栈」的接口

大白话就是 你能通过socket 的接口,来控制协议栈工作,从而实现网络通信,达到跨主机通信。

 

协议栈

协议栈的上半部分有两块,分别是负责收发数据的 TCP 和 UDP 协议,它们两会接受应用层的委托执行收发数据的操作。

协议栈的下面一半是用 IP 协议控制网络包收发操作,在互联网上传数据时,数据会被切分成一块块的网络包,而将网络包发送给对方的操作就是由 IP 负责的。

此外 IP 中还包括 ICMP 协议和 ARP 协议。

  • ICMP 用于告知网络包传送过程中产生的错误以及各种控制信息。
  • ARP 用于根据 IP 地址查询相应的以太网 MAC 地址。

IP 下面的网卡驱动程序负责控制网卡硬件,而最下面的网卡则负责完成实际的收发操作,也就是对网线中的信号执行发送和接收操作。

 

 

 

那具体 socket 有哪些接口呢?

socket 一般分为 TCP 网络编程和 UDP 网络编程。

 

TCP 网络编程

基于 TCP 协议的客户端和服务器工作

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将绑定在 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务器端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

 

服务端

import socket


server = socket.socket()  # 初始化一个服务端
"""
通过查看源码得知 
括号内不写参数默认就是基于网络的遵循TCP协议的套接字
"""
server.bind(('127.0.0.1', 8080))  # 绑定服务端(ip ,端口号)
"""
服务端应该具备的特征
    固定的地址
    ...  
127.0.0.1是计算机的本地回环地址 只有当前计算机本身可以访问 8080是端口号 我们写项目就输入8000以后的号码
"""
server.listen(5)  # 设置最大等待数5
"""
半链接池>>>:设置最大等待数 目的是节约资源 提高效率
"""
sock, addr = server.accept()  # 等待接收客户端(流式套接字)和客户端地址 如果没响应(程序阻塞)
"""
listen和accept对应TCP三次握手服务端的两个状态
流式套接字(SOCK_STREAM):该类套接字提供了面向连接的、可靠的、数据无错并且无重复的数据发送服务。而且发送的数据是按顺序接收的。
"""
print(addr)  # 客户端的地址
data = sock.recv(1024)  # 接收数据(流式套接字)
print(data.decode('utf8'))  # 打印传输过来的解码后的二进制数据
sock.send('你好啊'.encode('utf8'))  # 回传编码后的二进制数据
"""
recv和send接收和发送的都是bytes类型的数据
"""
sock.close()  # 终止传输
server.close()  # 终止服务端

半链接池

  1. 什么是半连接池:当服务器在响应了客户端的第一次请求后会进入等待状态,会等客户端发送的ack信息,这时候这个连接就称之为半连接
  2. 半连接池其实就是一个容器,系统会自动将半连接放入这个容器中,可以避免半连接过多 而保证资源耗光
  3. 产生半连接的两种情况:
    1. 客户端无法返回ACK信息
    2. 服务器来不及处理客户端的连接请求  就排队等候

客户端

import socket


client = 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()  # 关闭客户端

###########################
服务端与客户端首次交互
	一边是recv那么另一边必须是send  两边不能相同 否则就'冷战'了
###########################

 

通信循环

1.先解决消息固定的问题

    利用input获取用户输入


2.再解决通信循环的问题

    将双方用于数据交互的代码循环起来

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

 

代码优化及链接循环

1.发送消息不能为空
    统计长度并判断即可

2.反复重启服务端可能会报错>>>:address in use
  这个错在苹果电脑报的频繁 windows频率较少

在导入模块下导入一个包

from socket import SOL_SOCKET,SO_REUSEADDR

在bind前加

 server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)

3.链接循环

如果是windows 客户端异常退出之后服务端会直接报错
      处理方式:


          异常处理


  如果是mac或linux 服务端会接收到一个空消息
      处理方式:


          len判断

客户端如果异常断开 服务端代码应该重新回到accept等待新客户端

 

黏包问题

data1 = conn.recv(1024)
print(data1)
data2 = conn.recv(1024)
print(data2)
data3 = conn.recv(1024)
print(data3)

client.send(b'hello')
client.send(b'jason')
client.send(b'kevin')
"""
三次打印的结果
  b'hellojasonkevin'
  b''
  b''
  
  通过运行结果发现原本想要分三行数据输出的 变成了一行输出 这就是黏包现象
"""

 

疫情下的封闭管理  数据黏包??粘包??

 

考虑到:

TCP协议的特点
    会将数据量比较小并且时间间隔比较短的数据整合到一起发送

recv会产生黏包(如果recv接受的数据量(1024)小于发送的数据量,第一次只能接收规定的数据量1024,第二次接收剩余的数据量)

 

解决思路:

我们的核心问题是不知道即将要接收的数据多大
  如果能够精准的知道数据量多大 那么黏包问题就自动解决了!!!

 

解决办法:

使用struct模块 (精准获取数据大小)

struct 模块是一个可以将任意大小的数字转换成一个固定长度编码的模块

例如 13321111 通过q 模式 转化之后是8个字节
     133333   245  456   768  通过q 模式转化之后也是8个字节,不论数字大小
     但是这个转化对数字的大小范围有一定的要求

     i 模式转换的数字较小,转化之后的结果只有4个字节
     q 模式转换的数字范围较大,转换之后的结果有8个字节

    pack可以将任意长度的数字打包成固定长度
      unpack可以将固定长度的数字解包成打包之前数据真实的长度
  
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,)

 最终解决方案:

使用字典封装数据打包成固定的长度

服务端:

1.先创建一个字典

2.制作报头存入真实数据信息(数据大小、数据描述、文件名)

3.将字典打包

4.将固定长度的字典(数据)发送给对方

5.发送真实的数据

 

客户端

1.先接收字典的报头

2.解析得到字典数据长度

3.接收字典

4.解析字典中真实数据信息

5.接收真实数据

 

 

 

posted @ 2022-04-16 01:56  ji哩咕噜  阅读(32)  评论(0编辑  收藏  举报