一、两种网络架构

C/S Client与Server 客户端与服务器端架

从用户层面(物理层面)划分的

B/S Browser与Server 浏览器端与服务器端架构

从用户层面划分的
ip地址:主机的地址;port:精确到具体应用进程

二、 socket 定义

https://docs.python.org/3.9/library/socket.html
Socket(套接字)是计算机之间进行网络通信的一套程序接口,也是计算机进程间通信的一种方式,它可以实现不同之间进程的通信。
Socket层在应用层和运输层之间
在TCP/IP的网络架构中:
socket= ip+port ip标识主机的位置 ,port
第一个是 Socket,它提供了标准的 BSD Sockets API。
第二个是 SocketServer, 它提供了服务器中心类,可以简化网络服务器的开发。

windows上查看具体端口号的命令

  1. netstat -ano|findstr'端口号'
  2. netstat -ano

三、 socket 过程

简单过程

当客户端和服务器使用tcp协议进行通信时,客户端封装一个请求对象req,将请求对象req序列化成字节数组之后,然后通过套接字socket将字节数组发送发送到服务端,服务端通过套接字socket读取到字节数组,再反序列化成请求req,进行处理,处理完毕之后,生成一个响应res,将响应对象res序列化成字节数组,然后通过套接字将自己的数组发送给客户端,客户端通过套接字socket读取到自己数组,再反序列化成响应对象。
socket 模块 - 图1

细节过程

套接字其实只是一个引用(一个对象ID),这个套接字对象实际上是放在操作系统内核中。这个套接字对象内部有两个重要的缓冲结构,一个是读缓冲(read buffer),一个是写缓冲(write buffer),它们都是有限大小的数组结构。
当我们对客户端的socket写入字节数组时(序列化后的请求对象req),是将字节数组拷贝到内核区套接字对象的write buffer中,内核网络模块会有单独的线程负责不停的将write buffer中的数据拷贝到网络硬件,网卡硬件再将数据送到网线,经过一系列路由器交换机,最终送达服务器的网卡硬件中
服务器内核网络模块也会有单独的线程不停的将收到的数据拷贝到套接字的read buffer中等待用户层来读取。最终服务器的用户进程通过socket引用的read方法将read buffer中的数据拷贝到用户程序内存中,进行反序列化成请求对象进行处理,然后服务器将处理后的响应对象走一个相反的流程发送给客户端。

socket 模块 - 图2

阻塞

write buffer(读缓冲)中的空间时有限的,如果应用程序往套接字里面写的太块,这个空间会满,满了之后,写操作就会阻塞,直到整个空间有足够的位置腾出来。非阻塞IO(NIO),写操作不阻塞,能写多少写多少,通过放回值来确定能写入多少,没有写入的内容用户程序会缓存起来,后续继续重试写入。

req被拷贝到网卡的时候变成了大写的REQ?

因为这两个东西已经不是完全一样的了。内核的网络模块将缓冲区中的消息进行分块传输,如果缓冲区的内容太大,会被拆分成多个独立的小信息包。并且还要在每个信息包上附加一些额外的头信息,eg:原网卡地址和目的网卡地址、消息的序号等,到了接收端需要对这些消息包进行重新排序组装去头后才会扔进读缓冲中。

四、 socket模块介绍

语法格式:
socket.socket([family [, type [, protocol ]]])

4.1 参数

参数名称 说明
family 套接字中的网络协议:
AF_UNIX(基于文件类型的家族套接字,UNIX网域协议) AF_INET(基于网络类型的家族套接字,IPv4网域协议,eg:TCP与UDP)
type 套接字类型:
SOCK_STREAM(使用在TCP协议)
SOCK_DGRAM(使用在UDP协议)
SOCK_RAW(使用在IP协议)
SOCK_SEQPACKET(列表连接模式)
protocol 只使用在family等于AF_INET或type等于SOCK_RAW的时候。
protocol是一个常数,用于辨识所使用的协议种类。
默认值是0,表示适用于所有socket类型

4.2 方法

服务端socket方法 说明
s.bind(address) 将套接字绑定到地址, 在AF_INET下,以元组(host,port)的形式表示地址.
s.listen(backlog) 开始监听TCP传入连接。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
s.accept() 接受TCP连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
客户端socket方法
s.connect(address) 连接到address处的套接字。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex(adddress) 功能与connect(address)相同,但是成功返回0,失败返回errno的值。
公共socket方法 说明
s.recv(bufsize[,flag]) 接受TCP套接字的数据。数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。
s.send(string[,flag]) 发送TCP数据。将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。
s.sendall(string[,flag]) 完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvfrom(bufsize[.flag]) 接受UDP套接字的数据。与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
s.sendto(string[,flag],address) 发送UDP数据。将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close() 关闭套接字。
s.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
s.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr,port)
s.setsockopt(level,optname,value) 设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen]) 返回套接字选项的值。
s.settimeout(timeout) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
s.fileno() 返回套接字的文件描述符。
s.setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
s.makefile([mode [,bufsize]]) 创建一个与套接字有关的文件对象,mode和bufsize与内置函数open()相同
s.shutdown(how) 关闭联机的一端或两端。
如果how等于0,则关闭接收端
如果how等于1,则关闭传输段
如果how等于2,则同时关闭接收端和传输端

五、粘包和拆包

5.1 缓冲区

每个socket被创建后,都会分配两个缓冲区,输入缓冲区和输出缓存区。

write()/send()并不立即向网络中传输数据,而是先将数据写入缓冲区,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管有没有到达目标机器,不管何时被发送到网络,这些都是TCP协议负责的事情。

TCP协议独立于write()/send()函数,数据有可能刚被写入缓冲区就发送到网络,也可能再缓冲区中不断积压。
read()/recv()函数也是这样,也从输入缓冲区中读取数据,而不是直接从网络中读取。

5.2 I/O缓冲区特性

1.I/O缓冲区再每个TCP套接字中单独存在
2.I/O缓冲区再创建套接字时自动生成
3.即使关闭套接字也会继续传送出缓冲区中遗留的数据
4.关闭套接字将丢失输入缓冲区中的数据

5.3 发送粘包、拆包的原因

1.应用程序写入的数据大于套接字缓冲区的大小,将会发生拆包
2.应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,将会发生粘包3.进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度减TCP报文头部大于MSS的时候将发生拆包
4.接收方不及时读取套接字缓冲区的数据,将会发生粘包

5.4 处理粘包、拆包

1.使用带消息头的协议,消息头存储消息标识消息长度信息,服务端获取消息头,解析出消息长度,然后向后读取该长度的内容
2.设置定长消息,服务端每次读取既定长度的内容作为一条完整消息,当消息不够长时,空位补上固定字符
3.设置消息边界,将发送的每条消息的首尾都加上特殊标记符,前加“<”后加“>”
4.使用更加复杂的协议,eg:车联网的808、809协议struct模块,该模块可以解决

六、 socket 编程

6.1 TCP

TCP Socket是基于一种C/S的编程模型,服务端监听客户端的连接请求,一旦建立连接即可传输数据。

6.1.1 客户端编程

步骤:
1.创建socket,绑定套接字到本地IP与端口 socket.socket(socket.AF_INET,socket.SOCK_STREAM)
2.连接服务器 s.connect()
3.发送数据 s.sendall()
4.接收数据 s.recv()
5.关闭socket s.close()

  1. import socket
  2. import sys
  3. ##创建socket 对象
  4. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  5. ##获取本地主机名
  6. host = socket.gethostname()
  7. ##设置端口
  8. port = 9999
  9. ##连接服务,指定主机和端口
  10. s.connect((host, port))
  11. ##接收小于1024字节的数据
  12. msg = s.recv(1024)
  13. s.close()
  14. print(msg.decode('utf-8'))

6.1.2 服务端编程

1.打开socket socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2.绑定监听地址以及端口 s.bind()
3.监听连接 s.listen()
4.等待客户端连接 s.accept()
5.接收/发送数据 s.recv() , s.sendall()
6. 传输完毕后,关闭套接字 s.close()

  1. import socket
  2. import sys
  3. ##创建socket对象
  4. serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  5. ##获取本地主机名
  6. host = socket.gethostname()
  7. print(host)
  8. port = 9999
  9. ##绑定端口
  10. serversocket.bind((host, port))
  11. ##设置最大连接数,超过后排队
  12. serversocket.listen(5)
  13. while True:
  14. ##创建客户端连接
  15. clientsocket,addr = serversocket.accept()
  16. print("连接地址: %s"%str(addr))
  17. msg = '汉皇重色思倾国,御宇多年求不得。' +"\r\n"
  18. clientsocket.send(msg.encode('utf'))
  19. clientsocket.close()

需要先运行服务端
服务端
image.png
客户端显示
image.png

6.1.3 TCP 通信模型

image.png

6.1.4 简易聊天窗口

  1. ##服务器端
  2. import socket ##导入socket模块
  3. host = socket.gethostname() ##获取主机地址
  4. port = 12345 ##设置端口
  5. s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) ##创建TCP/IP套接字
  6. s.bind((host, port)) ##绑定地址(host, port)到套接字
  7. s.listen(1) ##设置最多连接数量
  8. sock,addr = s.accept() ##被动接收TCP客户端连接
  9. print('-----连接已经建立-----')
  10. info = sock.recv(1024).decode() ##接收客户端数据
  11. while info != 'byebye':
  12. if info:
  13. print('接收到的内容是:%s'%info)
  14. send_data = input("输入的消息是:") ##发送数据
  15. sock.send(send_data.encode()) ##发送TCP数据
  16. if send_data == 'byebye':
  17. print('-----服务端请求断开连接-----')
  18. break
  19. info = sock.recv(1024).decode() ##接收客户端数据
  20. sock.close() ##关闭客户端套接字
  21. s.close()
  22. print('-----连接已经断开-----')
  23. ##客户端
  24. import socket ##导入socket模块
  25. s = socket.socket() ##创建tcp/IP套接字
  26. host = socket.gethostname() ##获取主机地址
  27. port = 12345 ##设置端口号
  28. s.connect((host, port)) ##设置主动化TCP服务器联机
  29. print('------已连接------')
  30. info = ''
  31. while info != 'byebye': ##判断是否退出
  32. send_data = input('请输入发送内容:') ##输入内容
  33. s.send(send_data.encode()) ##发送TCP数据
  34. if send_data == 'byebye': ##判断是否退出
  35. print('-----客户端请求断开连接-----')
  36. break
  37. info = s.recv(1024).decode() ##接收服务端数据
  38. print('接收到的内容:'+info)
  39. s.close() ##关闭套接字
  40. print('------已断开------')

image.png
image.png

6.2UDP

UDP编程也需要将通信双方分为客户端和服务器。
服务器需要绑定端口但是不需要listen() 进行监听,直接接收来自任何客户端的数据。
客户端不需要调用connect() 与服务器进行连接,直接将数据发送给服务器。

6.2.1 服务端编程

  1. import socket
  2. s= socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ##UDPSocket
  3. s.bind((("127.0.0.1",10021))) ##端口绑定
  4. print('UDP连接')
  5. while True:
  6. ##获取数据和客户端的地址和端口,一次最多接收1024字节
  7. data,addr =s.recvfrom(1024)
  8. print("接收数据%s:%s"%addr)
  9. s.sendto(data.decode("utf-8").upper().encode(),addr) ##数据大写返回客户端
  10. ##不关闭socket

6.2.2 客户端编程

  1. import socket
  2. s =socket.socket(socket.AF_INET,socket.SOCK_DGRAM) ##UDP
  3. addr = ('127.0.0.1',10021) ##服务端地址
  4. while True:
  5. data =input("请输入要处理的内容:") ##获得数据
  6. if not data or data =='quit':
  7. break
  8. s.sendto(data.encode(),addr) ##发送到服务器
  9. recvdata,addr = s.recvfrom(1024) ##接收服务端发来的数据
  10. print(recvdata.decode("utf-8")) ##解码打印
  11. s.close() ##关闭socket

服务端
image.png
客户端
image.png

6.2.3 UDP 通信模型

image.png