0%

Python----socket编程

基于TCP的套接字编程:

socket层的位置:socket在传输层和应用层之间

socket是什么:

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

所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

socket的底层封装对文件描述符的读写。

套接字分类

  1. 基于文件类型的套接字家族;套接字家族的名字:AF_UNIX
  2. 基于网络类型的套接字家族;套接字家族的名字:AF_INET

套接字工作流程

先从服务端说起。服务端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务端的连接就建立了。客户端发送数据请求,服务器端接受请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

socket模块的应用:

服务端套接字函数

s.bind() 绑定(主机,端口号)到套接字、s.listen()开始TCP监听、s.accept()被动接受TCP客户的连接,(阻塞式)等待连接的到来。

accept成功之后,会创建一个连接connection对象,以及客户端的ip_port信息。

con.close()关闭这个连接。s.close()关闭服务器。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import socket

# 明确配置变量
ip_port = ('127.0.0.1', 8080)
back_log = 5
buffer_size = 1024
# 创建一个TCP套接字
ser = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 套接字类型AF_INET,socket.SOCK_STREAM, tcp协议,基于流式的协议
ser.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 对socket的配置重用ip和端口号
# 绑定端口号
ser.bind(ip_port) # 写哪个ip就要运行在哪台机器上
# 设置半连接池
ser.listen(back_log) # 最多可以连接多少个客户端
while True:
# 阻塞等待,创建连接
con, address = ser.accept() # 在这个位置进行等待,监听端口号
while True:
try:
# 接受套接字的大小,怎么发就怎么收
msg = con.recv(buffer_size)
if msg.decode('utf-8') == '1':
# 断开连接
con.close()
print('服务器收到消息', msg.decode('utf-8'))
except Exception as e:
break
con.close()
break
# 关闭服务器
ser.close()

简化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import socket

ip_port = ('127.0.0.1', 8080)
backlog = 5
buffer_size = 1024
ser = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ser.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ser.bind(ip_port)
ser.listen(backlog)
while True:
con, addr = ser.accept()
while True:
try:
msg = con.recv(buffer_size)
print("服务端收到的消息是", msg.decode('utf-8'))
con.send("hello".encode('utf-8'))
if msg.decode('utf-8') == 'q':
break
except:
break
con.close()
break
ser.close()

socket.SOL_SOCKET设置这个让我们可以在套接字级别上设置选项。选项有一些常用取值,常用选项有:

  • SO_BROADCAST广播消息的能力(只有udp支持广播,并且还必须是在支持广播消息的网络上(例如以太网,令牌环网等))、

  • SO_DEBUG,仅由TCP支持。当给一个TCP套接字开启本选项时,内核将为TCP在该套接字发送和接受的所有分组保留详细跟踪信息。这些信息保存在内核的某个环形缓冲区中,并可使用trpt程序进行检查。

  • SO_KEEPALIVE

    给一个TCP套接字设置保持存活选项后,如果2小时内在该套接字的任何一方向上都没有数据交换,TCP就自动给对端发送一个保持存活探测分节。这是一个对端必须相应的TCP分节,它会导致以下3种情况之一。

    (1)对端以期望的ACK响应。应用进程得不到通知(因为一切正常)。在又经过仍无动静的2小时后,TCP将发出另一个探测分节。

    (2)对端以RST响应,它告知本端TCP:对端已崩溃且已重新启动。该套接字的待处理错误被置为ECONNRESET,套接字本身则被关闭。

    (3)对端对保持存活探测分节没有任何响应。

    如果根本没有对TCP的探测分节的响应,该套接字的待处理错误就被置为ETIMEOUT,套接字本身则被关闭。然而如果该套接字收到一个ICMP错误作为某个探测分节的响应,那就返回响应的错误,套接字本身也被关闭。

    本选项的功能是检测对端主机是否崩溃或变的不可达(譬如拨号调制解调器连接掉线,电源发生故障等等)。如果对端进程崩溃,它的TCP将跨连接发送一个FIN,这可以通过调用select很容易的检测到。

    本选项一般由服务器使用,不过客户也可以使用。服务器使用本选项时因为他们花大部分时间阻塞在等待穿越TCP连接的输入上,也就是说在等待客户的请求。然而如果客户主机连接掉线,电源掉电或者系统崩溃,服务器进程将永远不会知道,并将继续等待永远不会到达的输入。我们称这种情况为半开连接。保持存活选项将检测出这些半开连接并终止他们。

  • SO_LINGER

    本选项指定close函数对面向连接的协议(例如TCP和SCTP,但不是UDP)如何操作。默认操作是close立即返回,但是如果有数据残留在套接字发送缓冲区中,系统将试着把这些数据发送给对端。

  • RCVBUF和SNDBUF接收和发送缓冲区

  • RCVLOWAT和SNDLOWAT接收和发送低水位

  • SO_RCVTIMEO 和 SO_SNDTIMEO套接字选项

  • SO_REUSEADDR 和 SO_REUSEPORT 套接字选项

    SO_REUSEADDR所有的TCP服务器都应该指定本套接字选项,一个最重要的原因如下:

    • SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知的端口,即使以前建立的将该端口用作他们的本地端口的连接仍存在。这个条件通常是这样碰到的:

      ​ (a)启动一个监听服务器;

      ​ (b)连接请求到达,派生一个子进程来处理这个客户;

      ​ (c)监听服务器终止,但子进程继续为现有连接上的客户提供服务;

      ​ (d)重启监听服务器。

      默认情况下,当监听服务器在步骤d通过调用socket,bind和listen重新启动时,由于他试图捆绑一个现有连接(即正由早先派生的那个子进程处理着的连接)上的端口,从而bind调用会失败。但是如果该服务器在socket和bind两个调用之间设置了SO_REUSEADDR套接字选项,那么将成功。所有TCP服务器都应该指定本套接字选项,以允许服务器在这种情况下被重新启动。

这里关于套接字选项不做更多展开,附上链接参考,https://blog.csdn.net/u010144805/article/details/78579771

客户端:p.connect(ip地址,端口号) 连接服务器

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import socket

p = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
p.connect(('127.0.0.1', 8080))
while 1:
msg = input('please input: ')
# 防止输入空消息
if not msg:
continue
p.send(msg.encode('utf-8')) # 收发消息一定要二进制,记得编码
if msg == '1':
break
p.close()

简化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
import socket

p = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
p.connect(('127.0.0.1', 8080))
while True:
msg = input('please input: ')
p.send(msg.encode('utf-8'))
data = p.recv(1024)
print(data.decode('utf-8'))
if msg == 'q':
break
p.close()
  • 用户态:专门存放应用程序。内核态:专门存放操作系统的内核。
  • 在socket 里面 from socket import * 这样可以减少代码量。
  • 当发送回车换行,内容为空,没有必要发所以就会卡顿。当自己这一端 的内核态没东西会卡住recv()。加一个判断是否为空解决。
  • 按照socket数据--->内核态----->网卡的顺序发送 send()()都是发送socket数据 send()和recv()都是往自己的内存里面收发。
  • 端口号+ip地址+mac地址 = 哪个应用程序+哪台电脑+哪个房间(一一对应) 标示互联网上唯一的一个程序

udp套接字

由于udp是无连接的所以比TCP更简洁。

服务端:recvfrom()接受的结果是发送的信息,和发送方的IP和端口号。sendto(信息,目标主机IP和端口号)

1
2
3
4
5
6
7
8
9
10
11
from socket import *

udp_ser = socket(AF_INET, SOCK_DGRAM) # 数据报式的套接字
udp_ser.bind(('127.0.0.1', 8080))

while True:
data = udp_ser.recvfrom(1024)
print(data) # (b'asd', ('127.0.0.1', 60606))
udp_ser.sendto('data'.encode('utf-8'), data[1])

# udp_ser.close()

简化代码如下:

1
2
3
4
5
6
7
8
9
10
11
import socket

udp_ser = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_ser.bind(('127.0.0.1', 8080))
while True:
data, addr = udp_ser.recvfrom(1024)
print(data, addr)
udp_ser.sendto('hello'.encode('utf-8'), addr)
if data.decode('utf-8') == 'q':
break
udp_ser.close()

客户端:

1
2
3
4
5
6
7
8
9
from socket import *

s = socket(AF_INET, SOCK_DGRAM)
while True:
msg = input('input-1: ')
s.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
data, addr = s.recvfrom(1024)
print(data.decode('utf-8'))
# s.close()

简化代码如下:

1
2
3
4
5
6
7
8
9
10
11
import socket

p = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
msg = input('please input: ')
p.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
data, addr = p.recvfrom(1024)
print(data.decode('utf-8'))
if msg == 'q':
break
p.close()

这里和tcp的socket区别可以看出,recv方法和recvfrom方法的一个区别

  • recv在自己这端缓冲区为空时,阻塞。
  • 而recvfrom在自己这端的缓冲区为空时,就收一个空。

用udp实现两个不同客户端的交流:

服务端:

1
2
3
4
5
6
7
8
9
10
11
from socket import *

udp_ser = socket(AF_INET, SOCK_DGRAM)
udp_ser.bind(('127.0.0.1', 8080))
data_1, addr_1 = udp_ser.recvfrom(1024)
data_2, addr_2 = udp_ser.recvfrom(1024)
while True:
data1 = udp_ser.recvfrom(1024)
udp_ser.sendto(data1[0], addr_2)
data2 = udp_ser.recvfrom(1024)
udp_ser.sendto(data2[0], addr_1)

客户端1:

1
2
3
4
5
6
7
8
9
from socket import *

s = socket(AF_INET, SOCK_DGRAM)
s.sendto('hello'.encode('utf-8'), ('127.0.0.1', 8080))
while True:
msg = input('input-1: ')
s.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
data, addr = s.recvfrom(1024)
print(data.decode('utf-8'))

客户端2:

1
2
3
4
5
6
7
8
9
from socket import *

s = socket(AF_INET, SOCK_DGRAM)
s.sendto('hello'.encode('utf-8'), ('127.0.0.1', 8080))
while True:
data, addr = s.recvfrom(1024)
print(data.decode('utf-8'))
msg = input('input-2: ')
s.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))

HTTP三次握手、四次挥手

三次握手(因为刚开始没有数据传输所以可以合并),四次挥手(因为客户端到服务端数据传完可以断开,但是服务端的数据不一定发完,所以不能一次性断开) 握手:

  1. 客户端发送连接请求(syn)
  2. 服务器回应确认发送(ack),第一条客户端到服务器端的连接建好,并且向客户端发送连接请求(syn)
  3. 客户端回应确认发送(ack),第二条服务器端到客户端的连接建好。

挥手(谁先发完,谁就断开连接):

  1. 客户端发送请求断开连接(seq)
  2. 服务端回应确认(ack)此时客户端到服务端的链接断开
  3. 服务端发送请求断开连接(seq)
  4. 客户端回应确认(ack) 此时服务端到客户端的链接断开

至此双向连接都已断开。

HTTP协议

特点:无状态的协议、基于请求/响应的模式。

POST方式有请求体,而GET方式没有请求体。在请求协议中,空行是用来和请求体分开。

部分参数:

1
2
3
4
5
6
7
Accpect:接收类型  */* 代表全部接受,q=0.8代表权重
Accpect-Encoding :可接受压缩格式
Accpect-Language:可接受的语言
Refer:防盗链,从哪里过来的
Connection: 3000毫秒的时间差
Host:主机地址
user-agent:请求头