Python Socket 网络编程

Python Socket 网络编程目录

  • 1 Socket简介
  • 2 Socket实现TCP通信
  • 3 Socket实现TCP连续消息发送
  • 4 Socket实例化参数的含义
  • 5 Socket实现UDP通信
  • 6 Socket的非阻塞模块实现TCP的多客户端连接
  • 7 Socket实现文件上传下载
  • 8 参考资料

1 Socket简介

socket是什么?

  • Socket是电脑网络中进程间数据流的端点
  • Socket是操作系统的通信机制
  • 应用程序通过Socket进行网络数据的传输

为了更好的理解Socket,首先要知道一个简单TCP的过程。下面是TCP建立连接时的三次握手的过程:
Python Socket 网络编程_第1张图片
经过这样的三次握手的过程,客户端与服务端之间将形成一条通路,此后数据将在这条通路上进行传输。
这与Socket通信过程有什么关系呢?看看Socket建立连接的过程:
Python Socket 网络编程_第2张图片
客户端与服务端通过Socket程序,经过如上的绑定监听连接确认等流程,最终会在二者之间建立一条信息传送的通路。
可以发现,Socket建立通信的过程与TCP非常相似。实际上, 这样的Socket使用的就是TCP协议。
而实际上,Socket可以使用的通信方式,不仅是TCP,它能适应多种网络协议,而Socket编程能在多种协议之间形成一种统一的编程方法。其中最主要的网络协议是两种:UDPTCP
Socekt编程是非常有用的,服务器端传输数据,大量涉及网络协议,而只要涉及网络协议,就离不开Socket应用。

2 Socket实现TCP通信

服务端程序: 创建一个Python脚本socekt_server.py,并进行编辑:

# 导入socket模块
import socket

# 创建实例(不指定参数,会使用默认参数,默认为TCP协议)
skt = socket.socket()

# 绑定监听的ip和port
ip_port = ('127.0.0.1', 8888)
skt.bind(ip_port)

# 设置一个最大连接数(可以没有,而直接进行消息接收)
# 这里的最大连接数是指socket进入监听之后的传入连接数,并不是同一时间有多个程序进行处理,socket是阻塞的,同一时间只有1个程序进行处理
# 即,这里的最大连接数是socket可以挂起的最大连接数,同时有多个请求来,可以进行同时的回应
# 但其中正在处理的程序只有1个,其余处于等待状态,等待正在处理的程序处理完成
# 如果有更多请求,服务器会直接拒绝,该值最小为1
skt.listen(5)

# 接受客户端的连接请求(在接受连接前(客户端发送请求前),accept是阻塞状态,为了体现,加入了一个提示信息,当连接建立,打印客户端的ip和port)
print('正在等待连接...')
conn, address = skt.accept()
print(address)

# 建立连接之后,服务端向客户端返回信息
# 需要注意的是,Python3以上,网络数据的发送接收都是byte类型
# 所以,如果数据是str类型,则需要进行编码
msg = "Hello, I'm server."
conn.send(msg.encode())

# 直接进行连接的关闭,不做其他处理(实际服务端应该针对客户端输入进行处理后再返回数据)
conn.close()

运行程序后命令行输出:
服务端输出

客户端程序: 创建一个Python脚本socekt_server.py,并进行编辑:

# 导入socket模块
import socket

# 实例初始化,仍然使用默认参数,为TCP连接
# 与服务端使用相同的通信协议,才能建立连接,进行通信
skt = socket.socket()

# 客服端连接的ip和端口必须是服务端所监听的ip和端口
ip_port = ('127.0.0.1', 8888)
skt.connect(ip_port)

# 建立连接之后,接收服务端信息并打印,接受缓冲区中1024个字节的数据
data = skt.recv(1024)
print(data.decode())

# 进行一次接收后即关闭连接
skt.close()

运行后,客户端收到服务端发送来的消息,并打印在命令行上:
Python Socket 网络编程_第3张图片
而服务端在命令行打印出建立连接的客户端的ip和port:
Python Socket 网络编程_第4张图片

3 Socket实现TCP连续消息发送

上面的服务端程序和客户端程序在建立连接后,进行了一次通信之后便终止了,为了实现二者之间连续的消息发送,需要改造服务端和客户端程序。
首先,服务端socekt_server.py中,对后半部分加上while True循环,使服务端能够不断地进行连接与断开:

# 导入socket模块
import socket

# 创建实例(不指定参数,会使用默认参数,默认为TCP协议)
skt = socket.socket()

# 绑定监听的ip和port
ip_port = ('127.0.0.1', 8888)
skt.bind(ip_port)

# 设置一个最大连接数(可以没有,而直接进行消息接收)
# 这里的最大连接数是指socket进入监听之后的传入连接数,并不是同一时间有多个程序进行处理,socket是阻塞的,同一时间只有1个程序进行处理
# 即,这里的最大连接数是socket可以挂起的最大连接数,同时有多个请求来,可以进行同时的回应
# 但其中正在处理的程序只有1个,其余处于等待状态,等待正在处理的程序处理完成
# 如果有更多请求,服务器会直接拒绝,该值最小为1
skt.listen(5)

# 不断循环,不断进行连接的建立与关闭
# 这种模拟是合理的,实际情况下,服务端程序不会终止
while True:
    # 接受客户端的连接请求(在接受连接前(客户端发送请求前),accept是阻塞状态,为了体现,加入一个提示信息,当连接建立,打印客户端的ip和port)
    print('正在等待连接...')
    conn, address = skt.accept()
    print(address)

    # 连接建立之后,服务端向客户端返回信息
    # 需要注意的是,Python3以上,网络数据的发送接收都是byte类型
    # 所以,如果数据是str类型,则需要进行编码
    msg = "Hello, I'm server."
    conn.send(msg.encode())

    # 直接进行连接的关闭,不做其他处理(实际服务端应该针对客户端输入进行处理后再返回数据)
    conn.close()

运行服务端程序,命令行显示:
Python Socket 网络编程_第5张图片
再运行客户端程序,发现客户端收到了服务端的信息并打印了出来,而后终止:
Python Socket 网络编程_第6张图片
此时服务端并没有终止,而是打印出客户端的ip和port之后,断开了本次连接,并重新开始等待新的客户端的连接请求:
Python Socket 网络编程_第7张图片
再次运行客户端,客户端有同样的输出:
Python Socket 网络编程_第8张图片
服务端则再次打印出了本次连接的客户端的ip和port,然后断开本次连接,并重新等待客户端的下一次连接请求:
Python Socket 网络编程_第9张图片
现在已经模拟了服务端与客户端不断建立连接、断开连接的过程,仍不能模拟某次连接中,连续发送消息的过程。现在定义的客户端和服务端,是非常简单的:

  • 服务端接受客户端的连接请求后,向客户端发送一条消息,便重新开始下一次连接等待
  • 客户端建立连接后,接受服务端发送的数据,便终止本次连接

如果想真正实现服务端与客户端在一次连接之中不断进行通信,需要进一步改造。
终止服务端程序。
服务端socekt_server.py

# 导入socket模块
import socket

# 创建服务端socket实例(不指定参数,会使用默认参数,默认为TCP协议)
skt = socket.socket()

# 绑定监听的ip和port
ip_port = ('127.0.0.1', 8888)
skt.bind(ip_port)

# 设置一个最大连接数(可以没有,而直接进行消息接收)
# 这里的最大连接数是指socket进入监听之后的传入连接数,并不是同一时间有多个程序进行处理,socket是阻塞的,同一时间只有1个程序进行处理
# 即,这里的最大连接数是socket可以挂起的最大连接数,同时有多个请求来,可以进行同时的回应
# 但其中正在处理的程序只有1个,其余处于等待状态,等待正在处理的程序处理完成
# 如果有更多请求,服务器会直接拒绝,该值最小为1
skt.listen(5)

# 不断进行连接的建立与关闭
# 这种模拟是合理的,因为实际情况下,服务端程序不会终止,只会有连接的断开
while True:
    # 接受客户端的连接请求,在接受连接前,也即客户端发送请求前,accept是阻塞状态
    # 为了体现,加入一个提示信息,连接建立后,打印客户端的ip和port
    print('正在等待连接...')
    conn, address = skt.accept()
    print('已与', address, '建立连接!')

    # 建立连接之后,服务端向客户端返回信息
    # 需要注意的是,Python3以上,网络数据的发送接收都是byte类型
    # 所以,如果数据是str类型,则需要进行编码
    conn.send("连接成功!".encode())

    # 不断接受客户端发送来的数据
    while True:
        # 接受并打印客户端消息(实际服务端应直接对接收的数据进行处理,而不用打印)
        data = conn.recv(1024)
        print('From Client:', data.decode())

        # 处理客户端发送来的数据
        if data == b'exit':
            break

        # 添加前缀,发送,并额外返回一条信息
        response = b'From Server: ' + data
        conn.send(response)
        conn.send(b'From Server: One more')

    # 本次连接关闭
    conn.close()

客户端socekt_client.py

# 导入socket模块
import socket

# 实例初始化,仍然使用默认参数,为TCP连接
# 与服务端使用相同的通信协议,才能建立连接,进行通信
skt = socket.socket()

# 客服端连接的ip和端口必须是服务端所监听的ip和端口
ip_port = ('127.0.0.1', 8888)
skt.connect(ip_port)

# 连接建立后,服务端会向客户端发送1条数据,进行接收并打印
data = skt.recv(1204)
print(data.decode())

# 不断发送消息
while True:
    msg_input = input("输入要发送的消息:")
    skt.send(msg_input.encode())

    if msg_input == 'exit':
        break

    # 接收服务端信息并打印,1024指接收缓冲区1024个字节的数据
    # 2次接收,因为服务端发送了2次数据
    data = skt.recv(1204)
    print(data.decode())
    data = skt.recv(1204)
    print(data.decode())

# 进行一次接收后即关闭连接
skt.close()

启动服务端,服务端显示正在等待连接:
启动服务端,服务端显示正在等待连接
启动客户端,连接建立,客户端收到服务端发送的连接成功的消息,并提示输入消息:
客户端收到服务端发送的连接成功的消息
此时服务端显示连接已建立:
Python Socket 网络编程_第10张图片
此时客户端输入一条消息,点击回车,会收到2条来自服务端的消息,并再次提示输入消息:
Python Socket 网络编程_第11张图片
服务端则成功收到来自客户端的消息:
Python Socket 网络编程_第12张图片
再次执行,有相同的结果,客户端:
Python Socket 网络编程_第13张图片
服务端:
Python Socket 网络编程_第14张图片
客户端输入exit,客户端便终止了:
Python Socket 网络编程_第15张图片
服务端则是断开本次连接,重新等待下次连接:
Python Socket 网络编程_第16张图片
重启一个客户端,客户端:
Python Socket 网络编程_第17张图片
服务端:
Python Socket 网络编程_第18张图片
此时如果再启动一个客户端,则新启动的客户端是没有输出的,因为服务端此时正与第一个客户端进行通信,第二个客户端不能与服务端建立连接:
Python Socket 网络编程_第19张图片
当第1个客户端退出后:
Python Socket 网络编程_第20张图片
第2个客户端与服务端建立连接:
Python Socket 网络编程_第21张图片
服务端对应输出:
Python Socket 网络编程_第22张图片

4 Socket实例化参数的含义

family:地址簇

  • socket.AF_INET IPv4(默认)
  • socket.AF_INET6 IPv6
  • socket.AF_UNIX 只能用于单一的Unix系统进程间通信

type:类型

  • socket.SOCK_STREAM 流式socket,for TCP(默认)
  • socket.SOCK_DGRAM 数据报式socket,for UDP
  • socket.SOCK_RAW 原始套接字
  • socket.SOCK_RDM 可靠UDP形式
  • socket.SOCK_SEQPACKET 可靠的连续数据报服务

proto:协议号

  • 0 默认,可以省略
  • CAN_RAW或CAN_BCM 地址簇为AF_CAN时

具体的,可以看看socket的构造函数:

def __init__(self, family=-1, type=-1, proto=-1, fileno=None):
	# For user code address family and type values are IntEnum members, but
	# for the underlying _socket.socket they're just integers. The
	# constructor of _socket.socket converts the given argument to an
	# integer automatically.
	if fileno is None:
		if family == -1:
			family = AF_INET
		if type == -1:
			type = SOCK_STREAM
		if proto == -1:
			proto = 0
	_socket.socket.__init__(self, family, type, proto, fileno)
	self._io_refs = 0
	self._closed = False

5 Socket实现UDP通信

服务端程序: 新建一个名为socket_server_udp.py的Python脚本,编辑:

import socket

# socket的通信方式为IPv4、数据报(UDP)
skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 服务端绑定ip和port
ip_port = ('127.0.0.1', 9999)
skt.bind(ip_port)

# 不断接收数据
while True:
   data = skt.recv(1024)
   print(data.decode())

# UDP的通信,不需要连接与关闭连接

客户端程序: 新建一个名为socket_client_udp.py的Python脚本,编辑:

import socket

# socket的通信方式为IPv4、数据报(UDP),与服务端一致
skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 目标ip和port
ip_port = ('127.0.0.1', 9999)

# 不断发送数据
while True:
    msg_input = input('输入要发送的信息:')
    if msg_input == 'exit':
        break
    # 因为没有建立连接,所以在发送时需要指定ip和port
    skt.sendto(msg_input.encode(), ip_port)

# 主动发送关闭信息给服务器端
# UDP并不保证安全通信,所以在一定时间内,客户端不进行通信后,服务端会关闭通信端口
# 不需要显示关闭,但为了资源有效利用,建议直接关闭
skt.close()

启动服务端,没有任何输出:
启动服务端
启动客户端,提示输入信息:
启动客户端
发送一条数据给服务端,客户端:
发送一条数据给服务端
服务端接收到数据:
服务端接收到数据
再发送一条,客户端:
Python Socket 网络编程_第23张图片
服务端再次接收到:
Python Socket 网络编程_第24张图片
再启动一个客户端:
再启动一个客户端
利用新启动的客户端发送一条消息,客户端:
Python Socket 网络编程_第25张图片
服务端收到了:
Python Socket 网络编程_第26张图片
此时,终止第1个客户端:
Python Socket 网络编程_第27张图片
服务端仍在监听:
Python Socket 网络编程_第28张图片
所以,因为UDP的通信方式不建立连接,所以UDP的通信方式可以进行多次数据发送、多客户端连接的。UDP并不关心接收到的消息是由谁发送的。

6 Socket的非阻塞模块实现TCP的多客户端连接

可以看到,TCP不能够实现多客户端连接。同一时间,只能有一个正在连接的客户端,其余客户端均处在阻塞状态。
如何解决呢?可以使用socket的非阻塞模块socketserver,这个模块使用多线程的方式,解决了这个问题。
服务端,新建一个名为socket_server_tcp的Python脚本,编辑:

import socketserver
import random

class MyServer(socketserver.BaseRequestHandler):
    # 如果handle方法出现报错,则会进行跳过
    # setup方法和finish方法无论如何都会执行
    # 一般只覆盖handle方法

    # 首先执行setup
    def setup(self):
        pass

    # 然后执行handle
    def handle(self):
        # 定义连接变量
        conn = self.request
        # 发送连接消息
        msg = '已连接!'
        conn.send(msg.encode())
        # 不断接收消息
        while True:
            data = conn.recv(1024)
            print(data.decode())
            if data == b'exit':
                break
            response = b'Server: ' + data
            conn.send(response)
            conn.send(str(random.randint(1, 1000)).encode())


    # 最后执行finish
    def finish(self):
        pass

if __name__ == '__main__':
    # 创建多线程TCPServer实例
    server = socketserver.ThreadingTCPServer(('127.0.0.1', 8888), MyServer)
    # 开启异步多线程,等待连接
    server.serve_forever()

启动,无输出:
Python Socket 网络编程_第29张图片
再启动一个tcp客户端:
启动一个tcp客户端
客户端向服务端发送一条消息:
Python Socket 网络编程_第30张图片
服务端收到了这条消息:
Python Socket 网络编程_第31张图片
再打开一个客户端:
Python Socket 网络编程_第32张图片
发送消息给服务端:
Python Socket 网络编程_第33张图片
服务端收到了第2个客户端发送的消息:
Python Socket 网络编程_第34张图片

7 Socket实现文件上传下载

运维的时候经常会遇到需要文件上传的情况,但没有第三方软件,此时可以自己实现。
文件接收: 新建一个名为文件接收.py的Python脚本,编辑:

# 场景:上传文件到服务器
# 文件接收端是服务器(服务端)

import socket

skt = socket.socket()
ip_port = ('127.0.0.1', 8888)
skt.bind(ip_port)
skt.listen(5)

while True:
    # 等待建立连接
    conn, address = skt.accept()
    # 一直使用当前连接进行数据发送,直到结束标志出现
    while True:
        with open('file', 'ab') as f:
            data = conn.recv(1024)
            if data == b'quit':
                break
            f.write(data)

    print('文件接收完成')
    conn.close()

文件发送: 新建一个名为文件发送.py的Python脚本,编辑:

# 场景:上传文件到服务器
# 文件发送端是本机(客户端)

import socket

# 创建socket实例并与服务端建立连接
skt = socket.socket()
ip_port = ('127.0.0.1', 8888)
skt.connect(ip_port)

# 文件分段上传
with open('./my_downloader.py', 'rb') as f:
    for i in f:
        skt.send(i)

# 传送完成后,非服务端发送结束信号
skt.send('quit'.encode())

先运行文件接收.py,再运行文件发送.py,发现文件发送程序很快就终止了,但文件接收程序始终在运行着,说明没有收到结束标志。
再看看收到的文件,最下方是:
在这里插入图片描述
也就是说,结束标志被接收在了文件中。
这就涉及了数据的粘包因为服务器端接收数据比较慢,而客户端直接将数据进行了发送,所以当服务端收到第一条信息时,客户端可能已经将全部信息发送完成。而服务端每次接收1024字节的数据,可能会将全部的数据进行接收。而此时,全部的数据并不仅有结束标志,所以服务端会认为这是一个数据内容,而进行文件的写入。
如何解决呢?
可以让服务端在每次接收完一段数据后,向客户端发送一个接收成功的标志,而客户端需要等待接收到这个标志,并进行判断,然后再进行下一段的传输。这样就阻止了数据流的大量涌入。
重新编辑文件接收.py

# 场景:上传文件到服务器
# 文件接收端是服务器(服务端)

import socket

skt = socket.socket()
ip_port = ('127.0.0.1', 8888)
skt.bind(ip_port)
skt.listen(5)

# 服务端不停机,每接收一个文件建立一个连接
while True:
    # 等待建立连接
    conn, address = skt.accept()

    # 一直使用当前连接进行数据发送,直到结束标志出现
    while True:
        with open('file', 'ab') as f:
            data = conn.recv(1024)
            if data == b'quit':
                break
            f.write(data)

        # 每次接收完毕,发送一个接收完成标志
        conn.send(b'success')

    print('文件接收完成')
    conn.close()

重新编辑文件发送.py

# 场景:上传文件到服务器
# 文件发送端是本机(客户端)

import socket

# 创建socket实例并与服务端建立连接
skt = socket.socket()
ip_port = ('127.0.0.1', 8888)
skt.connect(ip_port)

# 文件分段上传
with open('./my_downloader.py', 'rb') as f:
    for i in f:
        skt.send(i)
        # 每次发送完毕,等待服务端接收完成标志
        data = skt.recv(1024)
        if data != b'success':
            break

# 传送完成后,非服务端发送结束信号
skt.send('quit'.encode())

这样,依次运行文件接收程序与文件发送程序,得到的结果就是正确的了。文件接收程序不会终止,而只是断开连接并等待新连接的请求。文件发送程序在发送完毕后就终止了。

8 参考资料

慕课网 - python运维-Socket网络编程

你可能感兴趣的:(Python应用)