深入浅出谈socket

*现在我们开发往往不断使用封装好的web框架, 运行web服务也有相当多的容器, 但是其原理往往都离不开socket. 像是nginx底层就是采用类似python中epoll的异步监听方式加上socket结合来做. * 本文采取从最简单的socket通信实现聊天机器人, 到伪并发实现聊天机器人, 最后采用异步监听方式实现聊天机器人, 逐步推进.

首先我们实现一个最简单版的的socket服务端, server_s1.py

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

HOST='127.0.0.1'
PORT=9999

sockaddr=(HOST,PORT)
sk=socket.socket()
sk.bind(sockaddr)
sk.listen(5)
conn,address=sk.accept()
ret_bytes=conn.recv(1024)
print(str(ret_bytes,encoding='utf-8'))
conn.sendall(ret_bytes+bytes(', 已收到!',encoding='utf-8'))
sk.close()
  • sk=socket.socket() 这里创建socket对象
  • 通过sk.bind(sockaddr) 传入一个元组对象以此来设置服务端ip和port
  • sk.listen(5) 表示设置最大等待连接数为5个
  • conn,address=sk.accept() 此时阻塞进程, 循环等待被连接, 返回连接对象和包含连接信息的对象
  • ret_bytes=conn.recv(1024) 等待接受1024个字节的信息
  • conn.sendall(ret_bytes+bytes(', 已收到!',encoding='utf-8')) 将接受的信息加上 , 已收到! 重新发送给客户端. 注意, 在python2中可以传递str类型的数据, 但是在python3中只能传递byte类型的数据
  • sk.close() 关闭连接

至此简单的服务端已经写好了, 我们看看客户端, client_c1.py

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

HOST='127.0.0.1'
PORT=9999

sockaddr=(HOST,PORT)
ct=socket.socket()
ct.connect(sockaddr)
ct.sendall(bytes('第一次连接',encoding='utf-8'))
ret_bytes=ct.recv(1024)
print(str(ret_bytes,encoding='utf-8'))
ct.close()
  • 客户端中需要连接服务端, 通过ct.connect(sockaddr) 来执行

到现在为止, 已经把简单聊天机器人已经写好了, 客户端向服务端发送第一次连接 , 服务端接受输出到客户端并回馈给客户端第一次连接, 已收到! 接下来我们试着让这个服务端更健壮一些, 尝试让它可以不断的返回客户端发送过来的内容

这是第二个版本的服务端, server_s2.py

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

HOST='127.0.0.1'
PORT=9999

sockaddr=(HOST,PORT)
sk=socket.socket()
sk.bind(sockaddr)
sk.listen(5)
while True:
    conn,address=sk.accept()
    while True:
        try:
            ret_bytes=conn.recv(1024)
        except Exception as ex:
            print("已从",address,"断开")
            break
        else:
            conn.sendall(ret_bytes+bytes(', 已收到!',encoding='utf-8'))
sk.close()
  • 最内层的循环表示一旦连接则一直等待客户端发送消息并发回去, 直到连接断开
  • 最外层的循环表示即使断开连接但是服务器仍处于等待其他客户端连接
  • 加入异常处理表示, 客户端断开连接, 服务端仅仅断开此次连接

接下来看看客户端文件, client_c2.py

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

HOST='127.0.0.1'
PORT=9999

sockaddr=(HOST,PORT)
ct=socket.socket()
ct.connect(sockaddr)
while True:
    inp=input("请输入要发送的内容: ")
    ct.sendall(bytes(inp,encoding='utf-8'))
    ret_bytes=ct.recv(1024)
    print(str(ret_bytes,encoding='utf-8'))
ct.close()
  • 客户端仅仅需要将要发送内容的部分放到循环中即可

现在第二个版本已经可以连续不断的处理同一连接的消息, 即使断开也不会影响服务器的健壮性. 但是, 我们的服务器功能还很单一, 只能一次处理一个客户端的连接. 接下来将用select模块实现伪并发处理客户端连接

这里是第三个版本的服务端文件, server_s3.py

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

HOST = '127.0.0.1'
PORT = 9999

sockaddr = (HOST, PORT)

sk = socket.socket()
sk.bind(sockaddr)
sk.listen(5)

sk_inps = [sk, ]

while True:
    change_list, keep_list, error_list = select.select(sk_inps, [], sk_inps, 1)
    for sk_tmp in change_list:
        if sk_tmp == sk:
            conn, address = sk_tmp.accept()
            sk_inps.append(conn)
        else:
            try:
                ret_bytes = sk_tmp.recv(1024)
            except Exception as ex:
                sk_inps.remove(sk_tmp)
                print("已从", sk_tmp.getpeername(), "断开")
            else:
                sk_tmp.sendall(ret_bytes + bytes(', 已收到!', encoding='utf-8'))

    for sk_tmp in error_list:
        sk_inps.remove(sk_tmp)

sk.close()

我们首先来看一下循环的过程

循环过程
  • change_list, keep_list, error_list = select.select(sk_inps, [], sk_inps, 1) 中, select.select() 会自动监控起参数的内容, 当第一个参数中的对象发生变化时候会将该对象加到change_list中, 该次循环结束时change_list便会自动清空. 第一个参数中的变化对于sk对象, 这里只有客户端连接sk对象或者与sk对象断开两种情况
  • 接着我们遍历change_lis中的内容, 当有客户端连接时候, 如图所见, chang_list中只有sk对象, 此时我们将客户端的连接conn加入到sk_inps中, 让select下次循环时候也监控conn对象的变化
  • 当客户端发送消息时候意味着conn对象的变化, 此时change_list中加入该连接对象, 根据此对象, 我们可以处理客户端发送来的消息
  • 通过以上方式, 让服务端轮流处理每个客户端连接, 由于cpu现在的处理速度极快, 给人的感觉就是并发处理多个客户端请求, 实际上是伪装并发处理
  • sk_inps.remove(sk_tmp) 这一句中, 一旦客户端断开连接, 则服务端就会捕捉到异常并将该客户端对象从监控列表sk_inps中移除
  • 接着我们来说是select.select() 中的第二个参数, 该参数中有什么对象则keep_list 中就会加入什么对象, 该参数对于读写分离的伪并发处理有很大意义, 我们稍后再做介绍
  • select.select() 的第三个参数是当被监控的对象出现错误或者异常时候就将出错的对象加入到error_list 中, 随后我们遍历error_list并根据里边的出错对象将其从sk_inps中除去

该版本的客户端延续上一版本即可, 无需更改. 至此, 我们就建立一个能并发简单处理多客户端连接的服务器. 但是, 对于change_list 中遍历时候我们既有读又有写的操作, 这样当后期的处理复杂的时候, 代码维护很难再进行下去. 接下来我们接着开发我们的伪并发处理的最终版本

这里是服务的文件, server_s4.py

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

HOST = '127.0.0.1'
PORT = 9997

sockaddr = (HOST, PORT)

sk = socket.socket()
sk.bind(sockaddr)
sk.listen(5)

sk_inps = [sk, ]
sk_outs=[]
message_dic={}

while True:
    change_list, keep_list, error_list = select.select(sk_inps, sk_outs, sk_inps, 1)
    for sk_tmp in change_list:
        if sk_tmp == sk:
            conn, address = sk_tmp.accept()
            sk_inps.append(conn)
            message_dic[conn]=[]
        else:
            try:
                ret_bytes = sk_tmp.recv(1024)
            except Exception as ex:
                sk_inps.remove(sk_tmp)
                print("已从", sk_tmp.getpeername(), "断开")
                del message_dic[sk_tmp]
            else:
                sk_outs.append(sk_tmp)
                message_dic[sk_tmp].append(str(ret_bytes,encoding='utf-8'))

    for conn in keep_list:
        message= message_dic[conn][0]
        conn.sendall(bytes(message+", 已收到!",encoding='utf-8'))
        del message_dic[conn][0]
        sk_outs.remove(conn)

    for sk_tmp in error_list:
        sk_inps.remove(sk_tmp)

sk.close()
  • sk_outs=[] 中保存发送消息的客户端连接对象
  • message_dic={} 中保存消息内容
  • 当客户端发送消息时候, 我们在一个for循环中将其连接对象和消息内容分别保存起来, 在第二个循环中我处理消息内容

以上就是伪并发处理客户端请求所有内容, 究其本质其实是IO多路复用原理. 同时python中也提供了真正的并发处理模块socketserver, 下面我们采用socketserver来实现

首先看我们的服务端文件, server_s5.py

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

HOST = '127.0.0.1'
PORT = 9997

sockaddr = (HOST, PORT)

class MySocket(socketserver.BaseRequestHandler):
    def handle(self):
        conn = self.request
        while True:
            try:
                ret_bytes = conn.recv(1024)
            except Exception as ex:
                print("已从", self.client_address, "断开")
                break
            else:
                conn.sendall(ret_bytes + bytes(', 已收到!', encoding='utf-8'))


if __name__ == "__main__":
    server = socketserver.ThreadingTCPServer(sockaddr, MySocket)
    server.serve_forever()
  • 其原理只是将上述的IO多路复用改成了threading线程处理, 再加上本来的Socket内容形成
  • server = socketserver.ThreadingTCPServer(sockaddr, MySocket) 该句会将Socket服务端设置ip和port等内容封装到对象中, 执行初始化时候需要加入自己写的继承socketserver.BaseRequestHandler的类
  • server.serve_forever() 此句执行时候会使得对象调用handle(self) 方法, 在该方法中我们对客户端连接进行处理

以上我们将Socket从基础原理到复杂自定义已经使用封装好的模块使用介绍完毕. 接下来我们补充一些理论知识和常用的Socket参数和方法: **
首先我们来回顾一下OSI模型和TCP/IP协议簇,如图(
图片引自网络
)

OSI模型与TCP/IP协议簇

每层都有相对应的协议,但是socket API只是操作系统提供的一个用于网络编程的接口, 如图(图片引自网络)

socket与各层关系

根据 socket 传输数据方式的不同(其实就是使用协议的不同), 导致其与不同层打交道

  • Stream sockets, 是一种面向连接的 socket, 使用 TCP 协议.
  • Datagram sockets, 无连接的 socket,使用 UDP 协议.
  • Raw sockets, 通常用在路由器或其他网络设备中, 这种socket直接由网络层通向应用层.

以下是注意点:

  • 在我们创建对象时候sk=socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None) 实际上默认传入了参数, 第一个参数表示ip协议, ocket.AF_INET表示ipv4协议(默认就是), 第二个参数表示传输数据格式, socket.SOCK_STREAM表示tcp协议(默认就是), socket.SOCK_DGRAM表示udp协议
  • ret_bytes=conn.recv(1024) 中表示最多接受1024个字节; 若没有接受到内容则会阻塞进程, 等待接受内容
  • send() 可能会发送部分内容, sendall()本质就是内部循环调用send()直到将内容发送完毕, 建议使用sendall()
  • 当用socket做ftp文件传输时候会产生粘包问题, 此时只需在发送文件大小之后等待接受服务端返回一个确认码后, 再发送文件即可解决
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,649评论 18 139
  • 最近在学习Python看了一篇文章写得不错,是在脚本之家里的,原文如下,很有帮助: 一、网络知识的一些介绍 soc...
    qtruip阅读 2,700评论 0 6
  • 第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的“同行”Apache吧!Ngi...
    JokerW阅读 32,664评论 24 1,002
  • 今天晨读分享了《写出我心》这本书中三点写作技巧,下面谈谈我的感触。 关于写东西这件事,刚接触时我最大的感触就是,提...
    乐小源阅读 166评论 0 3
  • 无人与我立黄昏,无人问我粥可温。 无人与我捻熄灯,无人共我书半生。 无人陪我夜已深,无人与我把酒分。 无人拭我相思...
    李嘉乐乐乐阅读 430评论 2 7