python-网络编程-01-socket

博客教学1[译]Python 中的 Socket 编程(指南)sockerserver原理解析

一、scoket

socket 只是做为了一个接口,供用户以api使用,而不用直接操作tcp协议,极大的增加了开发的效率
在这里插入图片描述

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

1.1、套接字家族

# 基于文件类型的套接字家族:AF_UNIX
    unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

# 基于网络类型的套接字家族:AF_INET
    (还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候只使用AF_INET)

1.2、套接字工作流程

为了支持socket网络编程,Python 提供了两个级别访问网络的服务:

低级别的网络服务支持基本的`socket`模块,它提供了标准的`BSD Sockets API`,可以访问底层操作系统socket接口的全部方法。

高级别的网络服务模块`socketserver`,它提供了服务器中心类,可以简化网络服务器的开发。

socket逻辑通信
在这里插入图片描述
在这里插入图片描述

在Python中,import socket后,用socket.socket()方法来创建套接字,语法格式如下:

sk = socket.socket([family[, type[, proto]]])

参数说明:

  • family: 套接字家族,可以使AF_UNIX或者AF_INET
  • type: 套接字类型,根据是面向连接的还是非连接分为SOCK_STREAMSOCK_DGRAM,也就是TCP和UDP的区别。
  • protocol: 一般不填默认为0。

直接socket.socket(),则全部使用默认值。

1.3、套接字方法

socket类型 描述
socket.AF_UNIX 只能够用于单一的Unix系统进程间通信,基于文件类型的套接字
socket.AF_INET IPv4, 基于网络类型的套接字
socket.AF_INET6 IPv6
socket.SOCK_STREAM 流式socket , for TCP
socket.SOCK_DGRAM 数据报式socket , for UDP
socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
socket.SOCK_SEQPACKET 可靠的连续数据包服务
创建TCP Socket: s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
创建UDP Socket: s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

通过s = socket.socket()方法,我们可以获得一个socket对象s,也就是通常说的获取了一个“套接字”,该对象具有一下方法:

服务端方法 描述
s.bind() 绑定地址(host,port)到套接字,在AF_INET下,以元组(host,port)的形式表示地址。
s.listen(backlog) 开始监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
s.accept() 被动接受客户端连接,(阻塞式)等待连接的到来,并返回(conn,address)二元元组,其中conn是一个通信对象,可以用来接收和发送数据。address是连接客户端的地址。
客户端方法
s.connect(address) 客户端向服务端发起连接。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共方法
s.recv(bufsize) 接收数据,数据以bytes类型返回,bufsize指定要接收的最大数据量。
s.send() 发送数据。返回值是要发送的字节数量。
s.sendall() 完整发送数据。将数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvform() 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收的数据,address是发送数据的套接字地址。
s.sendto(data,address) 发送UDP数据,将数据data发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close() 关闭套接字,必须执行。
s.settimeout(timeout) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
s.fileno() 返回套接字的文件描述符。
s.setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
s.makefile() 创建一个与该套接字相关连的文件
s.getsockname() 获取连接的本地地址
s.getpeername() 获取连接的远程地址
socket.gethostname() 获取主机名称
socket.gethostbyname(name) 获取主机名称并解析成地址

注意事项:

  1. Python3以后,socket传递的都是bytes类型的数据,字符串需要先转换一下,string.encode()即可;另一端接收到的bytes数据想转换成字符串,只要bytes.decode()一下就可以。
  2. 在正常通信时,accept()recv()方法都是阻塞的。所谓的阻塞,指的是程序会暂停在那,一直等到有数据过来。

1.4、socket编程思路

服务端:

  1. 创建套接字,绑定套接字到本地IP与端口:socket.socket(socket.AF_INET,socket.SOCK_STREAM) , s.bind()
  2. 开始监听连接:s.listen()
  3. 进入循环,不断接受客户端的连接请求:s.accept()
  4. 接收传来的数据,或者发送数据给对方:s.recv() , s.sendall()
  5. 传输完毕后,关闭套接字:s.close()

客户端:

  1. 创建套接字,连接服务器地址:socket.socket(socket.AF_INET,socket.SOCK_STREAM) , s.connect(地址:port)
  2. 连接后发送数据和接收数据:s.sendall(), s.recv()
  3. 传输完毕后,关闭套接字:s.close()

TCP编程

# 服务端
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import socket

sk = socket.socket()            # 创建套接字
sk.bind(('127.0.0.1', 9999))                # 绑定服务地址
sk.listen(5)                    # 监听连接请求
print('启动socket服务,等待客户端连接...')
conn, address = sk.accept()     # 等待连接,此处自动阻塞
#( 三次握手建立的双向连接, (客户端 的IP,port) )
# print(conn)  
# <socket.socket fd=212, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9999), raddr=('127.0.0.1', 62679)>
while True:     # 一个死循环,直到客户端发送‘exit’的信号,才关闭连接
    client_data = conn.recv(1024).decode()      # 接收信息
    if client_data == "exit":       # 判断是否退出连接
        exit("通信结束")
    print("来自%s的客户端向你发来信息:%s" % (address, client_data))
    conn.sendall('服务器已经收到你的信息'.encode())    # 回馈信息给客户端
conn.close()    # 关闭连接


# 客户端 
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import socket
s = socket.socket()     			# 创建套接字
s.connect(('127.0.0.1', 9999))      # 连接服务器

while True:     # 通过一个死循环不断接收用户输入,并发送给服务器
    inp = input("请输入要发送的信息: ").strip()
    if not inp: continue     # 防止输入空信息,导致异常退出
    s.sendall(inp.encode())

    if inp == "exit": print("结束通信!")  break  # 如果输入的是‘exit’,表示断开连接

    server_reply = s.recv(1024).decode()
    print(server_reply)

s.close()       # 关闭连接

UDP编程

# 服务端
ss = socket()                       #创建一个服务器的套接字
ss.bind()                           #绑定服务器套接字
inf_loop:                           #服务器无限循环
    cs = ss.recvfrom()/ss.sendto()  # 对话(接收与发送)
ss.close()                          # 关闭服务器套接字

# 客户端 
cs = socket()   				# 创建客户套接字
comm_loop:      				# 通讯循环
    cs.sendto()/cs.recvfrom()   # 对话(发送/接收)
cs.close()                      # 关闭客户套接字

# 服务端
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 2000))

while True:
    msg, addr = sock.recvfrom(1024)
    print(msg, addr)
    sock.sendto("服务端收到数据 {}".format(msg).encode("utf-8"), addr)

# 客户端
import socket

st = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

ip_port = ("127.0.0.1", 2000)

while True:
    inp = input(">>>: ").strip()
    if not inp: continue

    st.sendto(inp.encode("utf-8"), ip_port)
    s_data, addr = st.recvfrom(1024)
    print(s_data.decode("utf-8"), addr)

socket.close()

二、socket–黏包

只有TCP有粘包现象,UDP永远不会粘包,一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。

	发送端可以是一K一K地发送数据,而接收端的应用程序可以2k/3k提走数据或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
    而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

# 怎样定义消息呢?
	可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

# 粘包问题?
	例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束, 所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
    发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

# 数据流协议
    TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难以分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

	UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

	tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头

# 消息说明 
	udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。

	tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

拆包

# 拆包的发生情况
	当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

# 补充问题一:为何tcp是可靠传输,udp是不可靠传输
	tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的, 而udp发送数据,对端是不会返回确认信息的,因此不可靠

# 补充问题二:send(字节流)和recv(1024)及sendall
	recv里指定的1024意思是从缓存里一次拿出1024个字节的数据
	send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失

2.1、不粘包的udp

# 服务端 ---  伪代码 
import socket
import subprocess

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ip_port = ("127.0.0.1", 2004)
server.bind(ip_port)

while True:
    data, addr = server.recvfrom(1024)
    res = subprocess.Popen("{}".format(data.decode("utf-8")), shell=True,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)

    server.sendto(res.stdout.read().decode("gbk").encode("utf-8"), addr)


# 客户端-- udp
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ip_port = ("127.0.0.1", 2004)

while True:
    inp = input(">>>>: ").strip()
    if not inp: continue
    server.sendto("{}".format(inp).encode("utf-8"), ip_port)
    data, addr = server.recvfrom(4096)
    print(data.decode("utf-8"))

2.2、low版tcp黏包

获取长度,并通过sendall 将数据全部发送在返回,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据,极度依赖于网络

#############################  服务端  #############################################
import socket
import subprocess

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 2005))
server.listen(3)

conn, addr = server.accept()
while True:
    data = conn.recv(512)

    if data == "exit": exit(3)

    res = subprocess.Popen("{}".format(data.decode("utf-8")), shell=True,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    stdout = res.stdout.read()
    stderr = res.stderr.read()
    sub_res = stderr if stderr else stdout
    data_length = len(sub_res)
    conn.send(str(data_length).encode("utf-8"))
    data = conn.recv(1024).decode("utf-8")
    if data == "recv_ready":
        # sendall 完整发送数据。将数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None
        conn.sendall(sub_res.decode("gbk").encode("utf-8"))

conn.close()

#############################  客户端  ############################################# 
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect_ex(("127.0.0.1", 2005))

while True:
    inp = input(">>>>: ").strip()
    if not inp: continue
    client.send("{}".format(inp).encode("utf-8"))
    length = int(client.recv(1024).decode("utf-8"))
    client.send("recv_ready".encode("utf-8"))
    send_size, recv_size = 0, 0
    data = b""
    while recv_size < length:
        data += client.recv(1024)
        recv_size += len(data)
    print(data.decode("utf-8"))

2.3、黏包解决

使用struct 获取长度, 与上一个相比较,少了一个传输,优化了一下网络传输速度,但还是比较low

#############################  服务端  #############################################
import socket
import struct
import subprocess

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("127.0.0.1", 2006))
s.listen(2)
conn, addr = s.accept()

while True:
    command = conn.recv(1024)
    # 获取客户端发过来的命令,并执行返回结果
    res = subprocess.Popen(command.decode("utf-8"), shell=True,
                           stderr=subprocess.PIPE,
                           stdout=subprocess.PIPE,
                           )
    stdout = res.stdout.read()      # 这里的获取最好与命令的stdout err一致
    stderr = res.stderr.read()
    # 1、添加报头长度
    length = len(stdout) + len(stderr)
    command_length = struct.pack("i", length)
    # 2、发送报头
    conn.send(command_length)
    # 3、发送数据
    conn.sendall(stdout)
    conn.sendall(stderr)

conn.close()

#############################  客户端  #############################################
import socket
import struct

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect_ex(("127.0.0.1", 2006))

while True:
    inp = input(">>>: ").strip()
    if not inp: continue
    # 1、发送命令
    client.send(inp.encode("utf-8"))
    # 2、获取报头长度
    header = client.recv(4)
    total_size = struct.unpack("i", header)[0]
    recv_size = 0       # 接收数据大小
    res = b""           # 数据拼接
    # 3、循环获取数据
    while recv_size < total_size:
        data = client.recv(1024)    # 1024-8096之间
        res += data     # 数据的拼接
        recv_size += len(data)  # 每次增加接收的长度
    print(res.decode("gbk"))

2.4、黏包(自定义报头)

服务端

#############################  服务端  #############################################
#coding:utf-8
#
import json
import struct
import socket
import subprocess

obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
obj.bind(('127.0.0.1', 2007))
obj.listen()

while True:
    conn, client = obj.accept()
    print("客户端地址: {}".format(client))

    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0: break

            obj = subprocess.Popen(data.decode("utf-8"), shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()

            # 制作一个命令的文件字典,用于传递文件的大小, 固定header的长度
            header_dic = {
    
    
                # 发送一个字典到客户端, 用于在操作缓存中读取数据
                "header_lenth": len(stdout) + len(stderr)   
            }
			# 将字典转换成字符串格式, 用于服务端之间传递数据
            header_json = json.dumps(header_dic).encode("utf-8") 
            # # 1、设置报头大小
            header = struct.pack("i", len(header_json))    # 以4个字节的固定长度发到客户端上,然后在操作字典
            # 2、将报头发给客户端
            conn.send(header)   # 先前字典的固定长度4个字节,带上命令的长度发送第一次
            conn.send(header_json)  # 第二次将字典发送过去
            conn.send(stderr)
            conn.send(stdout)

        except Exception:
            break

    conn.close()

obj.close()

#############################  客户端  #############################################
import json
import socket
import struct

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect_ex(("127.0.0.1", 2007))

while True:
    inp = input(">>>: ").strip()
    if not inp: continue
    client.send(inp.encode("utf-8"))
    # 1、获取报头,先接收第一个4个字节
    data = client.recv(4)
    total_size = struct.unpack("i", data)[0]
    # 2、 获取到字典的固定长度之后, 直接获取字典的大小
    header_dic = client.recv(total_size)
    # 接收到的是bytes格式,转换成utf-8格式, 然后在使用json
    header_json = json.loads(header_dic)

    recv_size = 0
    res = b""
    while recv_size < header_json["total_size"]:
        data = client.recv(1024)
        res += data
        recv_size += len(data)
    print(res.decode("gbk"))

三、socketserver

​ Python为了满足我们对多线程网络服务器的需求,提供了socketserver模块。socketserver 在内部使用IO多路复用以及多线程/进程机制 ,实现了并发处理多个客户端请求的socket服务端。每个客户端请求连接到服务器时,socketserver服务端都会创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。

示例-demo

# sockerserver创建说明
class Myhandle(socketserver.BaseRequestHandler):	# 必须继承sockerserver的基类
    def handle(self):								# 调用基类的函数
		self.request 			# 等于是  socket.accept 元组的第一个值conn

# --- __main__ -------
s = socketserver.ThreadingTCPServer((ip,port), MyServer)
	# 相当于 创建socker对象,bind, listen 
s.server_forever()  # 循环建立连接,每建立一个连接就会启动一个线程(专门与建立好的链接通信)
	# 相当于 while True:  s.accept()

#############################  服务端  #############################################
#coding:utf-8
#   服务端
import socketserver

class Myhandle(socketserver.BaseRequestHandler):
    def handle(self):
        print(self.request) # = 等于是  socket.accept 元组的第一个值conn
        while True:  # 通信循环
            try:
                data = self.request.recv(1024)
                print(self.client_address, data)
                if len(data) == 0:break
                self.request.send(data)
            except Exception:
                break


if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(("127.0.0.1",1234), Myhandle)
    server.serve_forever()

#############################  客户端  #############################################
#coding:utf-8
#   客户端
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1",1234))

while True:
    msg = input(">>>>>: ").strip()
    if len(msg) == 0: continue
    client.send(msg.encode("utf-8"))
    data = client.recv(1024)
    print(data)

client.close()

源码分析

在当前文件右键-》Diagrams-》Show Diagrams-》Python Class Diagrams. 点击UML图界面上方的m图片可以显示成员函数,点击f图标可以显示成员变量

# class Myhandle(socketserver.BaseRequestHandler):

class BaseRequestHandler:  # 继承类

    """Base class for request handler classes.

    This class is instantiated for each request to be handled.  The
    constructor sets the instance variables request, client_address
    and server, and then calls the handle() method.  To implement a
    specific service, all you need to do is to derive a class which
    defines a handle() method.
    这个类用于处理每个request的通信.构建类的时候需要request,客户地址和server对象 ,然后调用handle()方法.想完成一个特定的服务,需要继承此类然后定义一个handle()方法.
    The handle() method can find the request as self.request, the
    client address as self.client_address, and the server (in case it
    needs access to per-server information) as self.server.  Since a
    separate instance is created for each request, the handle() method
    can define other arbitrary instance variables.
    handle()方法里通过self.request找到request.由于针对每个request都生成独立的对象,handle()方法里还可以定义任意的变量.
    """

使用ThreadingTCPServer的要点:

  • 创建一个继承自socketserver.BaseRequestHandler的类;
  • 这个类中必须定义一个名字为handle的方法,不能是别的名字!
  • 将这个类,连同服务器的ip和端口,作为参数传递给ThreadingTCPServer()构造器
  • 手动启动ThreadingTCPServer。

ThreadingTCPServer这个类是一个支持多线程和TCP协议的socketserver,它的继承关系是这样的:

class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
                             多线程类      功能父类

​ 右边的TCPServer实际上是主要的功能父类,而左边的ThreadingMixIn则是实现了多线程的类, ThreadingTCPServer自己本身则没有任何代码。

MixIn在Python的类命名中很常见,称作“混入”,戏称“乱入”,通常为了某种重要功能被子类继承。

threadingMinIn源码分析

class ThreadingMixIn:

    daemon_threads = False

    def process_request_thread(self, request, client_address):      
        try:
            self.finish_request(request, client_address)
            self.shutdown_request(request)
        except:
            self.handle_error(request, client_address)
            self.shutdown_request(request)

    def process_request(self, request, client_address):

        t = threading.Thread(target = self.process_request_thread,
                             args = (request, client_address))
        t.daemon = self.daemon_threads
        t.start()

​ 在ThreadingMixIn类中,其实就定义了一个属性,两个方法。其中的process_request()方法实际调用的正是Python内置的多线程模块threading。这个模块是Python中所有多线程的基础,socketserver本质上也是利用了这个模块。

socketserver通过threading模块,实现了多线程任务处理能力,可以同时为多个客户提供服务。

​ 如果想同时连接多个客户端, 服务器无法同时对多个客户端提供服务。为什么会这样呢?因为Python的socket模块,默认情况下创建的是单进程单线程,同时只能处理一个连接请求,如果要实现多用户服务,那么需要使用多线程机制

下面我们使用Python内置的threading模块,配合socket模块创建多线程服务器

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import socket
import threading        # 导入线程模块


def link_handler(link, client):     
    """
    该函数为线程需要执行的函数,负责具体的服务器和客户端之间的通信工作
    :param link: 当前线程处理的连接
    :param client: 客户端ip和端口信息,一个二元元组
    :return: None
    """
    print("服务器开始接收来自[%s:%s]的请求...." % (client[0], client[1]))
    while True:     # 利用一个死循环,保持和客户端的通信状态
        client_data = link.recv(1024).decode()
        if client_data == "exit":
            print("结束与[%s:%s]的通信..." % (client[0], client[1]))
            break
        print("来自[%s:%s]的客户端向你发来信息:%s" % (client[0], client[1], client_data))
        link.sendall('服务器已经收到你的信息'.encode())
    link.close()


ip_port = ('127.0.0.1', 9999)
sk = socket.socket()            # 创建套接字
sk.bind(ip_port)                # 绑定服务地址
sk.listen(5)                    # 监听连接请求

print('启动socket服务,等待客户端连接...')

while True:     # 一个死循环,不断的接受客户端发来的连接请求
    conn, address = sk.accept()  # 等待连接,此处自动阻塞
    # 每当有新的连接过来,自动创建一个新的线程,
    # 并将连接对象和访问者的ip信息作为参数传递给线程的执行函数
    t = threading.Thread(target=link_handler, args=(conn, address))
    t.start()

demo_udp

#coding:utf-8
#   服务端
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议 udp
server.bind(("127.0.0.1",12346))

while True:
    data, client_add = server.recvfrom(1024)
    server.sendto(data, client_add)


#coding:utf-8
#  客户端
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议 udp

while True:
    msg = input(">>>>>: ").strip()
    server.sendto(msg.encode("utf-8"), ("127.0.0.1", 12346))
    data, server_add = server.recvfrom(1024)
    print(data)

,自动创建一个新的线程,
# 并将连接对象和访问者的ip信息作为参数传递给线程的执行函数
t = threading.Thread(target=link_handler, args=(conn, address))
t.start()


  demo_udp

```python
#coding:utf-8
#   服务端
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议 udp
server.bind(("127.0.0.1",12346))

while True:
    data, client_add = server.recvfrom(1024)
    server.sendto(data, client_add)


#coding:utf-8
#  客户端
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报协议 udp

while True:
    msg = input(">>>>>: ").strip()
    server.sendto(msg.encode("utf-8"), ("127.0.0.1", 12346))
    data, server_add = server.recvfrom(1024)
    print(data)

猜你喜欢

转载自blog.csdn.net/u010304195/article/details/113172377