Python3---网络编程

客户端/服务器架构

什么是客户端/服务器架构?

服务器就是一系列硬件或软件,为一个客户端(服务的用户)提供所需的“服务”。它存在的唯一目的就是等待客户端的请求,并响应他们(提供服务),然后等待更多请求。

客服端因特定的请求而联系服务器,并发送必要的数据,然后等待服务器的回应,最后完成请求或给出故障的原因。服务器无限的运行下去,并不断的处理请求;而客户端会对服务进行一次性请求,然后接收该服务,最后结束他们之间的事务。客户端在一段时间后可能会再次发出请求,但这些都被当做不同的事务。

客户端/服务器网络编程

在服务器响应客户端请求之前,必须进行一些初步的设置流程来为之后的工作做准备。首先会创建一个通信端点它能够使服务器监听请求。服务器就相当于公司的前台,一旦电话号码和设备安装成功且接线员到达时,服务就可以开始了。一旦一个通信端点已经建立,监听服务器就可以进入无限循环中,等待客户端的连接并响应它们的请求。

客户端所需要做的就是创建它的单一通信端点,然后建立一个到服务器的连接。客户端就可以发出请求,该请求包括任何必要的数据交换。一旦请求被服务器处理,且客户端收到结果或某种确认信息,此次通信就会被终止。

套接字:通信端点

套接字是计算机网络数据结构,它体现了上文所说的“通信端点”,在任何类型的通信开始之前,网络应用程序必须创建套接字。可以将它们比作电话插孔,没有它将无法进行通信。

套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信,这就是所谓的进程间通信。有两种套接字:基于文件的和面向网络的。

(1)AF_UNIX套接字:代表地址家族(address family):UNIX----基于文件

因为两个进程运行在同一台计算机上,所以这些套接字都是基于文件的,这意味着文件系统支持它们的底层基础结构。

(2)AF_INET套接字:代表地址家族:因特网----基于网络。

AF_INET:IPv4

AF_INET6:IPv6

套接字地址:主机-端口对

主机名和端口号就像区号和电话号码的组合一样。一个网络地址由主机名和端口号对组成。在通信时必须要有其他人在另一端接听;否则,你将听到“对不起,您所拨打的电话是空号,请核对后再拨”,或者在浏览网页时会出现“无法连接服务器,服务器没有响应或者服务不可达”

有效的端口号范围是:0~65535(0~1024预留给了系统)

面向连接的套接字和无连接的套接字

1.面向连接的套接字:

通信之前必须先建立一个连接,eg:使用电话系统给一个朋友打电话。这种类型的通信也称为虚拟电路流套接字

面向连接的通信提供序列化的,可靠的,不重复的数据交付,而没有记录边界。这基本上意味着每条消息可以拆分成多个片段,并且每一条消息片段都确保能够到达目的,然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序。

实现这种连接类型的主要协议是传输控制协议(TCP),TCP的套接字为SOCK_STREAM。

2.无连接的套接字

数据报类型的套接字,是一种无连接的套接字。这意味着在通信开始之前不需要建立连接。所以在数据传输过程中无法保证它的顺序性,可靠性或重复性。数据报保存了记录边界,这意味着消息是以整体发送的,并非首先分成多个片段,例如:使用面向连接的协议。

使用数据报的消息传输可以比作邮政服务。信件和包裹或许并不能以发送顺序到达。事实上,它们可能不会到达。为了将其添加到并发通信中,在网络中甚至有可能存在重复的消息。

那么,既然有这么多的副作用,为什么还要使用数据报呢?

由于面向连接的套接字所提供的保证,因此它们的设置以及对虚拟电路连接的维护需要大量的开销。然而,数据报不需要这些开销,即它的成本更加“低廉”,因此,它们通常能提供更好的性能并且可能适合一些类型的应用程序。

实现这种连接类型的主要协议是用户数据报协议(UDP),UDP套接字为SOCK_DGRAM。

Python中的网络编程

1.socket()模块函数

创建套接字,必须使用socket.socket()函数,它的一般语法如下:

其中,socket_family是AF_UNIX或AF_INET,socket_type是SOCK_STREAM或SOCK_DGRAM。protocol通常省略,默认为0.

所以,为了创建TCP/IP套接字,需要执行下面的方式调用socket.socket()。

同样,为了创建UCP/IP套接字,需要执行下面语句。

:在进行编程之前记得导入  socket模块---import socket)

2.套接字对象(内置)方法
常见套接字对象方法和属性
名称 描述
服务器套接字方法  
s.bind() 将地址(主机名,端口号对)绑定到套接字上
s.listen() 设置并启动TCP监听
s.accept() 被动接受TCP客户端连接,一直等待直到连接到达(阻塞)
客户端套接字方法  
s.connect() 主动发起TCP服务连接
s.connect_ex() connect()的扩展版本,此时会以错误码的形式返回问题,而不是抛出一个异常
普通的套接字方法  
s.recv() 接收TCP消息
s.recv_into() 接收TCP消息到指定的缓冲区(Python2.5中新增
s.send() 发送TCP消息
s,sendall() 完整的发送TCP消息
s.recvfrom() 接收UDP消息
s.recvfrom_into() 接收UDP消息到指定的缓冲区(Python2.5中新增
s.sendto() 发送UDP消息
s.getpeername() 连接套接字(TCP)的远程地址
s.getsockname() 当前套接字的地址
s,getsockopt() 返回给定套接字选项的值
s.setsockopt() 设置给定套接字选项的值
s.shutdown() 关闭连接
s.close() 关闭套接字
s.detach() 在未关闭文件描述符的情况下关闭套接字,返回文件描述符(Python3.2中新增
s.ioctl() 控制套接字的模式(Python2.6中新增,仅支持Windows。POSIX系统可以使用funcyl函数)
面向阻塞的套接字方法  
s.setblocking() 设置套接字的阻塞或非阻塞模式
s.settimeout() 设置套接字操作的超时时间(Python2.3中新增
s.gettimeout() 获取套接字的操作的超时时间(Python2.3中新增
面向文件的套接字方法  
s.fileno() 套接字的文件描述符
s.makefile() 创建与套接字关联的文件对象
数据属性  
s.family() 套接字家族(Python2.5中新增
s.type() 套接字类型(Python2.5中新增
s.proto() 套接字协议(Python2.5中新增

3.创建TCP服务器

伪代码:

ss = socket()#创建服务器套接字
ss.bind()#套接字与地址绑定
ss.listen()#监听连接
inf_loop:#服务器无限循环
    cs = ss.accpept()#接受客户端连接
    comm_loop:#通信循环
        # cs.recv()/cs.send()#对话(接收/发送)
    cs.close()#关闭客户端套接字
ss.close()#关闭服务器套接字(可选)

调用accept()函数后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接。默认情况下accept()是阻塞的,这意味着执行将被暂停,直到一个连接到达。(套接字也支持非阻塞模式的)

import socket
from time import ctime

tcpSerSock = socket.socket()
tcpSerSock.bind(('localhost',8020))
tcpSerSock.listen(5)#在连接被转接或拒绝之前,传入连接请求的最大数
while True:
    print('waiting for connection...')
    tcpCliSock,addr = tcpSerSock.accept() #如果有连接进来,则生成一个新的socket: tcpCliSock,并得到源的地址
    print('...connected from:',addr)
    while True:#开始接收数据,并处理
        data = tcpCliSock.recv(1024).decode('utf8')#将缓冲区的大小设置为为1K,可以根据网络性能和程序需要改变这个容量
        if not data:#如果没有数据了,则跳出while循环,等待下一次连接到来
            print('客户端断开连接了。。。')
            break
        print('[%s] %s' % (ctime(), data))#向shell输出,增加了当前的时间
        tcpCliSock.send(('[%s] %s' % (ctime(), data)).encode('utf8'))#将数据转换成bytes型,再send回去
    tcpCliSock.close() #关闭tcpCliSock,一次通信在服务器端的工作完成,继续等待下一次连接的到来
tcpSerSock.close() #不会被执行到,可以加个try-except进去,让程序更友好

一旦进入服务器的无限循环之中,我们就(被动的)等待客户端的连接。当一个连接请求出现时,我们进入对话循环中,在该循环中我们等待客户端发送的消息。如果消息是空白的,这意味着客户端已经退出,所以此时我们将跳出对话循环,关闭当前客户端的连接,然后等待另一个客户端连接 。如果确实得到了客户端发送的消息,就将其格式化并并返回相同的数据,但是会在这些数据中加上当前时间戳的前缀。最后一行永远不会执行,它只是用来提醒我们,如果写了一个处理程序来考虑一个更加优雅的退出方式,正如前面讨论的,应该调用close()方法。

4.创建TCP客户端

伪代码:
cs = socket()#创建客户端套接字
cs.connect()#尝试连接服务器
comm_loop:#通信循环
    cs.send()/cs.recv()#对话(发送/接收)
cs.close()#关闭客户端套接字

一旦客户端拥有了一个套接字,它就可以利用套接字的connect()方法直接创建一个到服务器的连接。当连接建立之后,它就可以参与到与服务器的一个对话中。最后,一旦客户端完成了它的事务,它就可以关闭套接字,终止此次连接。

import socket

tcpCliSock = socket.socket()
tcpCliSock.connect('localhost','8080')
while True:
    data = input('>')
    if not data:
        break
    tcpCliSock.send(data)
    data = tcpCliSock.recv(1024)
    if not data:
        break
    print(data.decode('utf-8'))
tcpCliSock.close()
客户端也有一个无限循环,但这并不意味着它会像服务器的循环一样永远的运行下去。客户端循环在以下两种条件下会跳出:用户没有输入,或者服务器终止且对recv()方法的调用失败。否则,在正常情况下,用户输入一些字符串数据,把这些数据发送到服务器进行处理。然后,客户端接收到了加了时间戳的字符串,并显示在屏幕上。

非阻塞套接字

普通套接字实现服务端有什么缺陷呢?

有没有想过要是开了一个服务器,却想要多个客户端能无缝转化,不影响彼此(在上面的代码中如果想实现,必须先关闭一个客户端,另一个客户端才会开始执行并返回数据,否则只会阻塞)

最大的缺陷就是:一次只能服务一个客户端

Python3---网络编程_第1张图片

import socket
server = socket.socket()
server.bind(('0.0.0.0',8080))
server.listen(5)

while True:
    connection,raddr = server.accept()#阻塞
    while True:
        recv_data = connection.recv(1024)#阻塞
        if recv_data:
            print(recv_data)
            connection.send(recv_data)
        else:
            connection.close()
            break

普通套接字实现的服务端的瓶颈:

accept()阻塞!在没有新的套接字来之前,不能处理已经建立的套接字请求

recv()阻塞!在没有接收到客户端请求数据之前,不能与其他客户端建立连接

可以用非阻塞套接字解决这个问题。

普通服务器的IO模型(会阻塞的IO模型)

Python3---网络编程_第2张图片

  阻塞IO(blocking IO)的特点:就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。

 什么是阻塞呢?想象这种情形,比如你等快递,但快递一直没来,你会怎么做?有两种方式:

  • 快递没来,我可以先去睡觉,然后快递来了给我打电话叫我去取就行了。
  • 快递没来,我就不停的给快递打电话说:擦,怎么还没来,给老子快点,直到快递来。

很显然,你无法忍受第二种方式,不仅耽搁自己的时间,也会让快递很想打你。

而在计算机世界,这两种情形就对应阻塞和非阻塞忙轮询。

  •  阻塞:数据没来,啥都不做,直到数据来了,才进行下一步的处理。
  • 非阻塞忙轮询:数据没来,进程就不停的去检测数据,直到数据来
非阻塞IO模型:

Python3---网络编程_第3张图片

非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有

非阻塞如何利用

  • 吃满 CPU !
  • 宁可用 while True ,也不要阻塞发呆!
  • 只要资源没到,就先做别的事!

服务端:

import socket

CONN_ADDR = ('127.0.0.1', 9999)
conn_list = []  # 连接列表
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 开启socket
sock.setblocking(False)  # 设置为非阻塞
sock.bind(CONN_ADDR)  # 绑定IP和端口到套接字
sock.listen(5)          # 监听,5表示客户端最大连接数
print('start listen')
while True:#第一层循环只负责生成对等连接套接字
    try:
        conn, addr = sock.accept()  # 被动接受TCP客户的连接,等待连接的到来,收不到时会报异常
        print('connect by ', addr)
        conn_list.append(conn)#保留已生成的对等连接套接字
        conn.setblocking(False)  # 设置非阻塞
    except BlockingIOError as e:
        pass

    tmp_list = [conn for conn in conn_list]
    for conn in tmp_list:#把所有生成的对等连接套接字都处理一遍
        try:
            data = conn.recv(1024) # 接收数据1024字节
            if data:
                print('收到的数据是{}'.format(data.decode()))
                conn.send(data)
            else:
                print('close conn',conn)
                conn.close()
                conn_list.remove(conn)#成功处理完一个对等连接套接字就移除
                print('还有客户端=>',len(conn_list))
        except IOError:
            pass

客户端:

import socket

client = socket.socket()
client.connect(('127.0.0.1', 9999))

while True:
    msg = input(">>>")
    if msg != 'q':
        client.send(msg.encode())
        data = client.recv(1024)
        print('收到的数据{}'.format(data.decode()))
    else:
        client.close()
        print('close client socket')
        break

输出结果:

start listen
connect by  ('127.0.0.1', 54908)
收到的数据是你好
connect by  ('127.0.0.1', 54910)
收到的数据是hi
connect by  ('127.0.0.1', 54912)
收到的数据是666
close conn 
还有客户端=> 2

 非阻塞IO模型优点:实现了同时服务多个客户端,能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在“”同时“”执行)。

 但是非阻塞IO模型绝不被推荐

非阻塞IO模型缺点:不停地轮询recv,占用较多的CPU资源。

                               对应BlockingIOError的异常处理也是无效的CPU花费 !

如何解决:IO多路复用

IO多路复用:

把socket交给操作系统去监控,相当于找个代理人(select), 去收快递。快递到了,就通知用户,用户自己去取。

阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用

Python3---网络编程_第4张图片

使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,感觉效率更差。

但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,

即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

epoll是目前Linux上效率最高的IO多路复用技术。

epoll是惰性的事件回调,惰性事件回调是由用户进程自己调用的,操作系统只起到通知的作用。

epoll实现并发服务器,处理多个客户端

Python3---网络编程_第5张图片

注册惰性事件回调:

Python3---网络编程_第6张图片

事件回调:

Python3---网络编程_第7张图片

import socket
import selectors

# 注册一个epllo事件
# 1. socket
# 2.事件可读
# 3.回调函数 把一个函数当成变量传到函数里

def recv_data(conn):
    data = conn.recv(1024)

    if data:
        print('接收的数据是:%s' % data.decode())
        conn.send(data)
    else:
        e_poll.unregister(conn)
        conn.close()

def acc_conn(p_server):
    conn, addr = p_server.accept()
    print('Connected by', addr)
    # 也有注册一个epoll
    e_poll.register(conn,selectors.EVENT_READ,recv_data)


CONN_ADDR = ('127.0.0.1', 9999)
server = socket.socket()
server.bind(CONN_ADDR)
server.listen(6) # 表示一个客户端最大的连接数

# 生成一个epllo选择器实例 I/O多路复用,监控多个socket连接
e_poll = selectors.EpollSelector() # window没有epoll使用selectors.DefaultSelector()实现多路复用
e_poll.register(server, selectors.EVENT_READ, acc_conn)

# 事件循环
while True:
    # 事件循环不断地调用select获取被激活的socket
    events = e_poll.select()
    #print(events)
    """[(SelectorKey(fileobj= < socket.socket
     laddr = ('127.0.0.1',9999) >,……data = < function acc_conn at 0xb71b96ec >), 1)]
    """
    for key, mask in events:
        call_back = key.data
        #print(key.data)
        call_back(key.fileobj)

运行结果:

Connected by ('127.0.0.1', 54914)
接收的数据是:你好
Connected by ('127.0.0.1', 54916)
接收的数据是:hi
Connected by ('127.0.0.1', 54918)
接收的数据是:123456

多路复用模型,使用select()的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多CPU。


部分参考自:https://www.cnblogs.com/xiao-apple36/p/8683198.html










你可能感兴趣的:(Python)