python学习笔记8--socket编程

一、socket

  Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原意那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。

  编写socket程序时需要进过如下步骤:

  1、服务端:

    a、实例化socket对象:server = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0)

      参数一:地址簇

      socket.AF_INET IPv4(默认)

      socket.AF_INET6 IPv6

      socket.AF_UNIX 只能够用于单一的Unix系统进程间通信

      参数二:类型

      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_RDM 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
      socket.SOCK_SEQPACKET 可靠的连续数据包服务

      参数三:协议

      0  (默认)与特定的地址家族相关的协议,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议

    b、设置监听的IP地址和端口号:server.bind(('IP',port))

    c、监听:server.listen()

    d、接受连接:conn,addr = server.accept()

    e、接受数据:data = server.recv(1024)

      1024:这个参数表示一次接收的数据包大小,即一次接收1024字节的包,官方建议最大设置为8192

    f、发送数据:server.send(xxxx)

      关于发送数据:发送的数据必须是bytes类型,所以在发送之前要对数据进行encode()转码

    g、关闭连接:server.close()

  2、客户端

    a、实例化socket对象:client = socket.socket()

    b、连接服务端:client.connect(("server IP",port))

    c、发送数据:client.send(xxxx)

    d、接收数据:data = client.recv(1024)

    e、关闭连接:client.close()

 

二、写个ssh

  我们可以自己写一个简单的ssh客户端程序出来,那么我们一步一步来实现:

  1、先写个简单的服务端和客户端  

import socket

server = socket.socket()
server.bind(("127.0.0.1",9999))
server.listen()
conn,addr = server.accept()
data = conn.recv(1024)
print("接收到了数据:",data.decode())
conn.send("你发来的数据我已经接收到了!".encode("utf-8"))
conn.close()
服务端
import socket

client = socket.socket()
client.connect(("127.0.0.1",9999))
data = input(">>: ")
client.send(data.encode("utf-8"))
recevd = client.recv(1024)
print("服务器响应信息:",recevd.decode())
client.close()
客户端

  运行一下,恩恩,不赖不赖,客户端真的把数据发出去了,服务端也真的把数据收到了。但是,客户端发一遍,服务端接一遍,然后程序就都退出了,显然,这不合理,OK,那我们来加个循环

import socket

server = socket.socket()
server.bind(("127.0.0.1",9999))
server.listen()
conn,addr = server.accept()
while True:
    data = conn.recv(1024)
    print("接收到了数据:",data.decode())
    conn.send("你发来的数据我已经接收到了!".encode("utf-8"))
conn.close()
服务端1.0
import socket

client = socket.socket()
client.connect(("127.0.0.1",9999))
while True:
    data = input(">>: ")
    client.send(data.encode("utf-8"))
    recevd = client.recv(1024)
    print("服务器响应信息:",recevd.decode())
client.close()
客户端1.0

  再运行一下,很好,已经可以不断的收发数据了,接下来我们让服务端执行客户端的命令,并把命令结果返回

import socket,os

server = socket.socket()
server.bind(("127.0.0.1",9999))
server.listen()
conn,addr = server.accept()
while True:
    print("准备好接收数据了")
    data = conn.recv(1024)
    print("接收到了命令:",data.decode())
    cmd_res = os.popen(data.decode()).read()
    conn.send("你发来的数据我已经接收到了!".encode("utf-8"))
    conn.send(cmd_res.encode("utf-8"))
conn.close()
服务端2.0
import socket

client = socket.socket()
client.connect(("127.0.0.1",9999))
while True:
    data = input(">>: ")
    client.send(data.encode("utf-8"))
    recevd = client.recv(1024)
    print("服务器响应信息:",recevd.decode())
    cmd_res = client.recv(1024)
    print(cmd_res.decode())
client.close()
客户端2.0

  运行一下,发现还不错,常规的命令,像ls什么的都可以了,但是,有很多蛋疼的地方:

  输入空命令或错误命令,客户端和服务端都会卡住

  一个服务端只能为一个客户端提供服务,客户端退出,服务端也退出

  针对这些问题,我们再做一些改进:

import socket,os

server = socket.socket()
server.bind(("127.0.0.1",9999))
server.listen()
while True:
    conn,addr = server.accept()
    while True:
        print("准备好接收数据了")
        data = conn.recv(1024)
        if not data:
            break
        print("接收到了命令:",data.decode())
        cmd_res = os.popen(data.decode()).read()
        if len(cmd_res) == 0:
            conn.send("命令输入有误".encode("utf-8"))
        else:
            conn.send(cmd_res.encode("utf-8"))
conn.close()
服务端3.0
import socket

client = socket.socket()
client.connect(("127.0.0.1",9999))
while True:
    data = input(">>: ").strip()
    if len(data) == 0:continue
    client.send(data.encode("utf-8"))
    cmd_res = client.recv(1024)
    print(cmd_res.decode())
client.close()
客户端3.0

   到目前为止,一切都看起来很不错了,客户端输入空命令,错误命令都不会有任何问题,但是还有个问题,当服务端发送的数据量大于1024时,悲剧了,客户端不会一次接收完,而是在客户端向服务端发送了新的请求后,服务端才吧上次没发完的数据发送过去,这问题就严重了,那应该怎么解决呢?我们想,客户端如果能提前知道服务端要发多少数据,那客户端就可以决定接收多少次,以此来保证数据全部接收完成。思路正确,那我们就让服务端在发送正常数据之前,先向客户端发送一下要发送的数据的大小:

import socket,os

server = socket.socket()
server.bind(("127.0.0.1",9999))
server.listen()
while True:
    conn,addr = server.accept()
    while True:
        print("准备好接收数据了")
        data = conn.recv(1024)
        if not data:
            break
        print("接收到了命令:",data.decode())
        cmd_res = os.popen(data.decode()).read()
        if len(cmd_res) == 0:
            conn.send("命令输入有误".encode("utf-8"))
        else:
            send_len = len(cmd_res.encode("utf-8"))
            conn.send(str(send_len).encode("utf-8"))
            conn.send(cmd_res.encode("utf-8"))
conn.close()
服务端4.0 
import socket

client = socket.socket()
client.connect(("127.0.0.1",9999))
while True:
    data = input(">>: ").strip()
    if len(data) == 0:continue
    client.send(data.encode("utf-8"))
    len_cmd_res = client.recv(1024).decode()
    received_len = 0
    data_rec = b""
    while received_len < int(len_cmd_res):
        cmd_res = client.recv(1024)
        data_rec += cmd_res
        received_len += len(cmd_res)
    print(data_rec.decode())
client.close()
客户端4.0

   我们运行上述代码,然后查看一些较大的文件,我们可以看到,此时客户端就可以一次性把所有的数据都接收到了,但是多试几次就会发现一个问题,会有报错如下:

Traceback (most recent call last):
  File "/Users/zhanghaoyan/PycharmProjects/day08/pritice_client.py", line 15, in <module>
    while received_len < int(len_cmd_res):
ValueError: invalid literal for int() with base 10: '1366一、动态导入模块\n\timport importlib\n\n\tmod = ........

   从报错的信息可以看出,服务端在两次发送数据的过程中并没有将数据长度和数据分开发送,而是一起发送给了客户端,而且这种现象不是一直存在,而是偶尔就出现一次,这种现象称之为 粘包 ,为什么会出现粘包现象呢?

  出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

  既然出现了粘包,那我们就需要想方设法避免这个问题,那如何避免呢?着手点有两个:

  1、利用TCP/IP协议发包的超时时间,即超时后强制发包

  2、另一种是将两个发包过程分开来,这样就不会出现粘包现象了 

   根据第一种摄像,我们可以让两次发包过程中间等待一个时间,这样就能避免粘包:

import socket,os,time

server = socket.socket()
server.bind(("127.0.0.1",9999))
server.listen()
while True:
    conn,addr = server.accept()
    while True:
        print("准备好接收数据了")
        data = conn.recv(1024)
        if not data:
            break
        print("接收到了命令:",data.decode())
        cmd_res = os.popen(data.decode()).read()
        if len(cmd_res) == 0:
            conn.send("命令输入有误".encode("utf-8"))
        else:
            send_len = len(cmd_res.encode("utf-8"))
            conn.send(str(send_len).encode("utf-8"))
            time.sleep(0.5)
            conn.send(cmd_res.encode("utf-8"))
conn.close()

  我们运行代码可以看到,这样做以后就完全没有粘包现象出现了,但是程序执行上确多了一个0.5秒的耗时,单次执行的时候这0.5秒似乎并不是什么问题,但是,如果程序很大,频繁的发包呢?那这个时间成本将会变得非常高,手动拉低程序运行速度这种事显然是与社会主义和谐社会背道而驰的,我们不能这么干。

  既然第一种方案被我们否决了,那第二种方案该怎么实现呢?我们怎么样让两次发包的过程分开而又不增加过多的消耗呢?其实我们可以在两次发包中间在加入一个接收客户端请求的动作,然后让客户端在收到第一个数据包后发送一个确认信号,然后服务端再进行接下来的发送:

  服务端:

import socket,os,time

server = socket.socket()
server.bind(("127.0.0.1",9999))
server.listen()
while True:
    conn,addr = server.accept()
    while True:
        print("准备好接收数据了")
        data = conn.recv(1024)
        if not data:
            break
        print("接收到了命令:",data.decode())
        cmd_res = os.popen(data.decode()).read()
        if len(cmd_res) == 0:
            conn.send("命令输入有误".encode("utf-8"))
        else:
            send_len = len(cmd_res.encode("utf-8"))
            conn.send(str(send_len).encode("utf-8"))
            ack = conn.recv(1024)
            conn.send(cmd_res.encode("utf-8"))
conn.close()

 

   客户端:

import socket

client = socket.socket()
client.connect(("127.0.0.1",9999))
while True:
    data = input(">>: ").strip()
    if len(data) == 0:continue
    client.send(data.encode("utf-8"))
    len_cmd_res = client.recv(1024).decode()
    client.send(b"OK")
    received_len = 0
    data_rec = b""
    while received_len < int(len_cmd_res):
        cmd_res = client.recv(1024)
        data_rec += cmd_res
        received_len += len(cmd_res)
    print(data_rec.decode())
client.close()

 

   OK!Well done!我们的ssh简单版算是制作完成了,但是这个代码还没法执行top这样的命令,原因在于我们的命令需要执行结束后,才能将结果返回,关于这点,以后再说吧。

 三、socketserver

  SocketServer内部使用 IO多路复用 以及 “多线程” 和 “多进程” ,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个“线程”或者“进程” 专门负责处理当前客户端的所有请求。

  socketserver模块有以下几种类型:

  socketserver.BaseServer():BaseServer不直接对外服务。

  socketserver.TCPServer():用于使用TCP协议的连接

  socketserver.UDPServer():用于使用UDP协议的连接

  socketserver.UnixStreamServer():只在Unix环境下使用

  socketserver.UnixDatagramServer():只在Unix环境下使用

  创建一个socketserver需要以下几步:

  1、创建一个类,该类继承socketserver.BaseRequestHandler并重构该类的handle()方法;

  2、选择使用一个类型来实例化一个对象,并将(IP地址,端口号),上述类名作为参数传入

  3、调用handle_request()(一般是调用其他事件循环或者使用select())或serve_forever()

  4、调用server_close()关闭server

  注意:让你的socketserver并发起来, 必须选择使用以下一个多并发的类

  class socketserver.ForkingTCPServer

  class socketserver.ForkingUDPServer

  class socketserver.ThreadingTCPServer

  class socketserver.ThreadingUDPServer

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

class Mysocket(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            try:
                self.date = self.request.recv(1024)
                print("接收到了:",self.date)
                self.request.send(self.date.upper())
            except ConnectionResetError as e:
                print("error:",e)
                break

if __name__ == "__main__":
    HOST,PORT = "0.0.0.0",9999
    obj = socketserver.TCPServer((HOST,PORT),Mysocket)
    obj.serve_forever()
Socketserver-Server
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import socket

client = socket.socket()
client.connect(("localhost",9999))
while True:
    cmd = input(">>:").strip()
    client.send(cmd.encode("utf-8"))
    date_rec_len = client.recv(1024)

    print(date_rec_len.decode())
Socketserver-Client

  如果想支持并发,那就将上述server端代码中的 obj = socketserver.TCPServer((HOST,PORT),Mysocket) 换成 obj = socketserver.ThreadingTCPServer((HOST,PORT),Mysocket)就可以了。

  接下来我们来看下BaseServer中有哪些方法吧:

 1 BaseServer.fileno():返回服务器监听套接字的整数文件描述符。通常用来传递给select.select(), 以允许一个进程监视多个服务器。
 2 
 3 BaseServer.handle_request():处理单个请求。处理顺序:get_request(), verify_request(), process_request()。如果用户提供handle()方法抛出异常,将调用服务器的handle_error()方法。如果self.timeout内没有请求收到, 将调用handle_timeout()并返回handle_request()。
 4 
 5 BaseServer.serve_forever(poll_interval=0.5): 处理请求,直到一个明确的shutdown()请求。每poll_interval秒轮询一次shutdown。忽略self.timeout。如果你需要做周期性的任务,建议放置在其他线程。
 6 
 7 BaseServer.shutdown():告诉serve_forever()循环停止并等待其停止。python2.6版本。
 8 
 9 BaseServer.address_family: 地址家族,比如socket.AF_INET和socket.AF_UNIX。
10 
11 BaseServer.RequestHandlerClass:用户提供的请求处理类,这个类为每个请求创建实例。
12 
13 BaseServer.server_address:服务器侦听的地址。格式根据协议家族地址的各不相同,请参阅socket模块的文档。
14 
15 BaseServer.socketSocket:服务器上侦听传入的请求socket对象的服务器。
16 
17 服务器类支持下面的类变量:
18 
19 BaseServer.allow_reuse_address:服务器是否允许地址的重用。默认为false ,并且可在子类中更改。
20 
21 BaseServer.request_queue_size:请求队列的大小。如果单个请求需要很长的时间来处理,服务器忙时请求被放置到队列中,最多可以放request_queue_size个。一旦队列已满,来自客户端的请求将得到 “Connection denied”错误。默认值通常为5 ,但可以被子类覆盖。
22 
23 BaseServer.socket_type:服务器使用的套接字类型; socket.SOCK_STREAM和socket.SOCK_DGRAM等。
24 
25 BaseServer.timeout:超时时间,以秒为单位,或 None表示没有超时。如果handle_request()在timeout内没有收到请求,将调用handle_timeout()。
26 
27 下面方法可以被子类重载,它们对服务器对象的外部用户没有影响。
28 
29 BaseServer.finish_request():实际处理RequestHandlerClass发起的请求并调用其handle()方法。 常用。
30 
31 BaseServer.get_request():接受socket请求,并返回二元组包含要用于与客户端通信的新socket对象,以及客户端的地址。
32 
33 BaseServer.handle_error(request, client_address):如果RequestHandlerClass的handle()方法抛出异常时调用。默认操作是打印traceback到标准输出,并继续处理其他请求。
34 
35 BaseServer.handle_timeout():超时处理。默认对于forking服务器是收集退出的子进程状态,threading服务器则什么都不做。
36 
37 BaseServer.process_request(request, client_address) :调用finish_request()创建RequestHandlerClass的实例。如果需要,此功能可以创建新的进程或线程来处理请求,ForkingMixIn和ThreadingMixIn类做到这点。常用。
38 
39 BaseServer.server_activate():通过服务器的构造函数来激活服务器。默认的行为只是监听服务器套接字。可重载。
40 
41 BaseServer.server_bind():通过服务器的构造函数中调用绑定socket到所需的地址。可重载。
42 
43 BaseServer.verify_request(request, client_address):返回一个布尔值,如果该值为True ,则该请求将被处理,反之请求将被拒绝。此功能可以重写来实现对服务器的访问控制。默认的实现始终返回True。client_address可以限定客户端,比如只处理指定ip区间的请求。 常用。
BaseServer相关方法和属性

 

posted @ 2016-09-21 11:01  没有手艺的手艺人  阅读(135)  评论(0编辑  收藏  举报