网络编程

socket

socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。

socket 与 file 的却别:

  • file 是对指定文件进行打开,读写,关闭
  • socket 是对服务器和客户端的 socket 进行打开,读写,关闭

一、socket 客户端与服务器交互流程

简单实例

服务端将客户端发送的字符串转成大写后再返回给客户端

import socket

sk = socket.socket()

sk.bind(('127.0.0.1', 8001))

sk.listen(5)

connect, address = sk.accept()

while True:
    receive_data = connect.recv(1024)

    if not receive_data:
        break

    connect.sendall(receive_data.upper())

connect.close()
服务端代码
import socket

sk = socket.socket()

sk.connect(('127.0.0.1', 8001))

while True:
    msg = input(">>>").strip()
    if not msg:
        continue
    sk.sendall(bytes(msg, encoding='utf8'))

    receive_data = sk.recv(1024)

    print(receive_data.decode())

sk.close()
客户端代码

服务器与客户端交互时不能发送空字符串

二、socket 类

sk = socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

family:

  • socket.AF_INET  默认值,IPv4
  • socket.AF_INET6  IPv6

type:

  • socket.SOCK_STREAM  默认值,TCP
  • socket.SOCK_DGRAM  UDP

proto: 默认值 0 ,一般不填

三、socket 对象方法

  • sk.bind(address)  绑定地址(IP,Port)到套接字,在 AF_INET 下以元组(IP,Port)的形式表示
  • sk.listen(backlog)  开始 TCP 监听,backlog 指定在拒绝连接之前,操作系统可以挂起的最大连接数,该值至少为 1,一般情况下设置为 5 即可
  • sk.setblocking(bool)  是否阻塞,默认值为 True, 如果设置为 False,一旦 accept 和 recv 没有数据,则报错
  • sk.accept()  被动接收客户端的连接(阻塞式),等待客户端连接,返回(sock, addr) , sock 为新的套接字对象,可以用来接收和发送消息,addr 为客户端的地址
  • sk.connect(address)   连接到 address 所在的服务器,address 以元组的形式表示(IP,Port) or (HOST,Port)
  • sk.connect_ex(address)  和 sk.connect() 类似,只是会有返回值,而不是抛出异常,如果连接成功返回 0
  • sk.close() 关闭套接字
  • sk.recv(bufsize[,flag]) 接收 socket 数据,数据以 bytes 返回(python2.x为 str), bufsize 指定一次可以接收的最大数据,flag 忽略,默认值为 0
  • sk.recvform(bufsize[,flag])  接收 socket 数据,返回值为 (data, address), data 为 bytes 类型的数据, address 为从哪个客户端接收的数据
  • sk.send(bytes[,flag]) 将 bytes 数据发送给 socket,返回值为发送的字节大小
  • sk.sendall(bytes[,flag])  将 bytes 数据发送给 socket,如果成功则返回 None,失败则抛出异常。
  • sk.sendto(bytes,address) 将 bytes 数据发送给 socket,通常用于 UDP
  • sk.settimeout(value)  设置 blocking 的超时时间,value 为一个浮点数,单位为 s,默认为不超时,通常配置在 socket 建立之初。value = 5,表示客户端最多等待 5 s
  • sk.getpeername() 返回套接字远端的地址,(IP, Port)
  • sk.getsockname() 返回自身的地址 (IP,Port)
  • sk.fileno() 返回 socket 的文件描述符,返回值为一个数字,-1 表示获取失败,通常在 select.select() 中使用

四、使用 socket 实现简单的 ssh 操作

粘包问题:

  sk.recv(bufsize) 在接收 socket 数据时,是有大小限制的,如果 send 的数据过大,就会出现一次性接收不完全的情况,导致后面的交互数据混乱

解决方法:

  在发送数据之前,先将需要发送的 bytes 大小发送给对方,等待对方准备完成后,在将数据发送。而接收方会根据 bytes 的大小一直接收,并进行数据组合

import socket
import subprocess

s = socket.socket()

s.bind(("127.0.0.1",9999))

s.listen(5)

while True:

    print("Wating New Connection ... ")

    conn, addr = s.accept()

    print("\tWating IP: {0[0]} Port: {0[1]}".format(addr))

    while True:

        # 捕获客户端发送过来的数据,最大接收 1024 字节
        recv_data = conn.recv(1024)

        # 如果接收到的自己为空,则跳出循环
        if not recv_data:break

        # 捕获苦短发送的命令
        cmd = str(recv_data, encoding='utf8')
        res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        output = str(res.stdout.read(), encoding='utf8')
        error = str(res.stderr.read(), encoding='utf8')
        if error:
            if output:
                send_data = "%s\n%s" % (output, error)
            else:
                send_data = error
        else:
            send_data = output
        print(len(send_data))
        # 如果需要发送的字节大约 1024,则先发送一个 Ready|{字节大小},作为标记,然后再紧接着发送数据
        if len(send_data) > 1024:
            ready_tag = "Ready|%s" % len(send_data)
            conn.send(bytes(ready_tag, encoding='utf8'))
            start_tag = conn.recv(1024)
            if start_tag.deconde() == 'Start':
                conn.send(bytes(send_data, encoding='utf8'))
        else:
            conn.send(bytes(send_data, encoding='utf8'))

    conn.close()
    print("\tIP:{0[0]} Port:{0[1]} Close Connection".format(addr))
服务端代码
import re
import socket

s = socket.socket()

s.connect(('127.0.0.1', 9999))

while True:
    try:
        # 捕获输入
        send_data = input(">> ").strip()

        # 如果捕获数据为空,则继续提示输入
        if not send_data:continue

        # 如果输入的数据为 exit,则跳出循环
        if send_data == 'exit':break

        # 发送捕获到的数据,python 3.5 之后发送的数据必须为 bytes 类型
        s.send(bytes(send_data, encoding='utf8'))

        # 接收最大 1024 字节的数据
        recv_data = s.recv(1024)

        # 将数据转换为 str
        recv_data = str(recv_data, encoding='utf-8')

        # 如果接收到的数据为匹配正则: Ready\|\d+$, 说明后面会有一个大约 1024的数据进行发送
        # 获取到需要接收的字节大小,然后进行多次接收,并进行组合,然后再输出
        if re.match('Ready\|\d+$', recv_data):
            # 获取数据的字节大小
            msg_size = recv_data.split("|")[-1]
            msg_size = int(msg_size)

            recv_data = b""     # 接受的数据
            recv_size = 0       # 已接收数据大小

            s.send(bytes("Start", encoding='utf-8'))

            while recv_size < msg_size:
                recv_msg = s.recv(1024)     # 多次接收
                recv_data += recv_msg       # 组合数据
                recv_size += len(recv_msg)  # 修改已接收数据大小

            recv_data = str(recv_data, encoding='utf-8')

        # 打印数据
        print(recv_data)
    except KeyboardInterrupt:
        break
s.close()
客户端代码

IO多路复用

通过一种机制监视多个文件描述符,一但某个描述符就绪(一般是读就绪或写就绪)能够通知程序做相应的读写操作

Linux中的 select,poll,epoll 都是IO多路复用的机制。

Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用。

网络操作、文件操作、终端操作等均属于IO操作,对于windows只支持Socket操作,其他系统支持其他IO操作,但是无法检测 普通文件操作 自动上次读取是否已经变化。

 

select 方法

readlist, writelist, errorlist = select.select(rlistwlistxlist[, timeout])

rlist  必选参数,监听读事件

wlist 必选参数,监听写事件

xlist 必选参数,监听异常事件

timeout 可选参数,select 阻塞的超时事件,如指定时间内监听的所有时间均无变化,则返回 3 个空list,如果 timeout 未指定则一直阻塞,知道监听到变化

socketserver 

通过 socket 创建的 socker 服务器默认只支持一个连接的操作,当已经存在一个连接时,新来的连接会处于等待状态(sk.listen(5),最多支持 5 个客户端等待),当已有连接断开后才能连接和下一个客户端进行连接。

socketserver 模块内部使用 “IO多路复用” 和 “多线程”、“多进程” 方式实现多并发的 socket

import socketserver


class Server(socketserver.BaseRequestHandler):

    # 这里的方法必须是 handle
    def handle(self):

        # 定义与客户端的所有交互内容
        while True:
            receive_data = self.request.recv(1024)
            if not receive_data:
                break

            send_data = receive_data.upper()
            self.request.send(send_data)


if __name__ == '__main__':
    # 创建 TreadingTCPServer 对象,并执行 serve_forever 方法
    server = socketserver.ThreadingTCPServer(('127.0.0.1',9998), Server)
    server.serve_forever()
服务端代码
import socket


s = socket.socket()

s.connect(('127.0.0.1',9998))

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

    if inp == 'quit':
        break

    s.send(bytes(inp, encoding='utf-8'))
    receive_data = s.recv(1024)
    print(receive_data.decode())


s.close()
客户端代码

一、ThreadingTCPSever 源码剖析

 

二、ThreadingTCPServer 源码精简版

import socket
import select
import threading


def process(request, client_ip):

    print("HOST[{0[0]}] Port[{0[1]}] Connect".format(client_ip))
    request.sendall(bytes('欢迎连接到服务器。。。', encoding='utf-8'))

    while True:
        receive_data = request.recv(1024)
        if not receive_data:
            break

        request.sendall(receive_data.upper())


sk = socket.socket()
sk.bind(('127.0.0.1',8001))
sk.listen(5)

while True:
    # 监听 socket 是否有 IO 变化
    r, w, e = select.select([sk], [], [], 1)
    if sk in r:
        request, client_ip = sk.accept()
        # 为 socket 创建多线程
        t = threading.Thread(target=process, args=(request, client_ip))
        t.start()
sk.close()
代码精简

 

 

posted @ 2016-09-25 19:44  wenchong  阅读(146)  评论(0编辑  收藏  举报