Socket网络编程

一、什么是Socket

  网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
  Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。

二、Socket参数介绍

  在python中,实例化一个socket连接对象后,需要传入的参数有以下几项(括号中是默认参数):              
socket.socket(family=AF_INETtype=SOCK_STREAMproto=0fileno=None

Socket Families(地址簇)

socket.AF_UNIX unix本机进程间通信 

socket.AF_INET IPV4 

socket.AF_INET6  IPV6

Socket Types

socket.SOCK_STREAM  #for tcp

socket.SOCK_DGRAM   #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 #废弃了

剩下的参数proto,fileno一般我们不用管。

三、socket实例

最简单的socket实例

import socket

client = socket.socket()
client.connect(('localhost',9999))
client.send(b'hello world')
data = client.recv(1024)
print('recv',data)

client.close()
socket_client
import socket

server = socket.socket()
server.bind(('localhost',9999))
server.listen()
conn,addr = server.accept()
data = conn.recv(1024)
print('recv:',data)
conn.send(data.upper())

server.close()
socket_server

  上面的代码的有一个问题, 就是SocketServer.py运行起来后, 接收了一次客户端的data就退出了。。。, 但实际场景中,一个连接建立起来后,可能要进行多次往返的通信。

import socket

client = socket.socket()
client.connect(('localhost',8888))
while True:
    data = input('>>请输入要发送的内容:')
    if len(data) == 0:
        continue
    client.send(data.encode('utf-8'))
    res = client.recv(1024)
    print(res.decode())

client.close()
socket_client 支持多次交互
import socket

server = socket.socket()
server.bind(('localhost',8888))
server.listen(5)
print('我要开始等电话了')
conn,addr = server.accept()
print('电话来了')
while True:
    data = conn.recv(1024)
    print(data)
    if not data:
        print('client has lost')
        break
    conn.send(data.upper())

conn.close()
socket_server 支持多次交互

  但是上面这种写法还是有缺陷的,虽然这种写法可以实现客户端和服务器端多次交互了,但是只要客户端断开了,服务器端也会断开,因为只有一个while循环,客户端一断开,服务器端收不到数据,就会直接break跳出循环,这样程序就结束了。这还不是我们想要的结果,我们想要的是,客户端断开了,服务器端可以接收下一个连进来的连接,然后和下一个连进来的客户端再次的进行多次的交互。

socket_client 支持多次交互
socket_server 支持多次交互
  那么知道了socket的实现原理之后,我们可以通过socket实现一个极简版的ssh工具,就是客户端连接上服务器后,让服务器执行命令,并返回结果给客户端。
import socket

client = socket.socket()
client.connect(('localhost',8888))
while True:
    data = input('>>请输入要发送的内容:')
    if len(data) == 0:
        continue
    client.send(data.encode('utf-8'))
    res_size = client.recv(1024)
    print(res_size)
    res_data = b''
    recv_size = 0
    while recv_size < int(res_size.decode()):
        data = client.recv(1024)
        recv_size += len(data)
        res_data += data
    else:
        print(recv_size)
        print(res_data.decode())

    print(res_data)

client.close()
socket_client_ssh
import socket
import os

server = socket.socket()
server.bind(('localhost',8888))
server.listen(5)
print('我要开始等电话了')
while True:
    conn,addr = server.accept()
    print('电话来了')
    while True:
        data = conn.recv(1024)
        if not data:
            print('client has lost')
            break
        res = os.popen(data.decode()).read()
        conn.send(str(len(res.encode())).encode())
        conn.send(res.encode())
        print('send done')

    conn.close()
socket_server_ssh

输出结果:

  看程序执行报错了, 我在客户端本想只接服务器端命令的执行结果大小,但实际上却连命令结果也跟着接收了一部分。

  这里就引入了一个重要的概念,“粘包”, 即服务器端你调用时send 2次,但你send调用时,数据其实并没有立刻被发送给客户端,而是放到了系统的socket发送缓冲区里,等缓冲区满了、或者数据等待超时了,数据才会被send到客户端,这样就把好几次的小数据拼成一个大数据,统一发送到客户端了,这么做的目地是为了提高io利用效率,一次性发送总比连发好几次效率高。 但也带来一个问题,就是“粘包”,即2次或多次的数据粘在了一起统一发送了。就是我们上面看到的情况 。 

  我们在这里必须要想办法把粘包分开, 因为不分开,你就没办法取出来服务器端返回的命令执行结果的大小。首先你是没办法让缓冲区强制刷新把数据发给客户端的。 你能做的,只有一个。就是让缓冲区超时,超时了,系统就不会等缓冲区满了,会直接把数据发走,那么如何让缓冲区超时呢?

答案就是:

  1. time.sleep(0.5),经多次测试,让服务器程序sleep 至少0.5就会造成缓冲区超时。虽然我们觉得0.5s不多,但是对数据实时要求高的业务场景,比如股票交易等用这种方法肯定是不行的。
  2.  不用sleep,服务器端每发送一个数据给客户端,就立刻等待客户端进行回应,即调用 conn.recv(1024), 由于recv在接收不到数据时是阻塞的,这样就会造成,服务器端接收不到客户端的响应,就不会执行后面的conn.sendall(命令结果)的指令,收到客户端响应后,再发送命令结果时,缓冲区就已经被清空了,因为上一次的数据已经被强制发到客户端了。看下面代码实现。  
import socket

client = socket.socket()
client.connect(('localhost',8888))
while True:
    data = input('>>请输入要发送的内容:')
    if len(data) == 0:
        continue
    client.send(data.encode('utf-8'))
    res_size = client.recv(1024)
    client.send(b'ok')
    print(res_size)
    res_data = b''
    recv_size = 0
    while recv_size < int(res_size.decode()):
        data = client.recv(1024)
        recv_size += len(data)
        res_data += data
    else:
        print(recv_size)
        print(res_data.decode())

client.close()
socket_client_ssh
import socket
import os

server = socket.socket()
server.bind(('localhost',8888))
server.listen(5)
print('我要开始等电话了')
while True:
    conn,addr = server.accept()
    print('电话来了')
    while True:
        data = conn.recv(1024)
        if not data:
            print('client has lost')
            break
        res = os.popen(data.decode()).read()
        conn.send(str(len(res.encode())).encode())
        conn.recv(1024)
        conn.sendall(res.encode())
        print('send done')

    conn.close()
socket_serevr_ssh

SocketServer

socketserver一共有这么几种类型

1
class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)

This uses the Internet TCP protocol, which provides for continuous streams of data between the client and server. 

1
class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)

This uses datagrams, which are discrete packets of information that may arrive out of order or be lost while in transit. The parameters are the same as for TCPServer

1
2
class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixDatagramServer(server_address, RequestHandlerClass,bind_and_activate=True)

There are five classes in an inheritance diagram, four of which represent synchronous servers of four types:

+------------+
| BaseServer |
+------------+
      |
      v
+-----------+        +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+        +------------------+
      |
      v
+-----------+        +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+        +--------------------+


创建一个socketserver 至少分以下几步:

  1. First, you must create a request handler class by subclassing the BaseRequestHandler class and overriding its handle() method; this method will process incoming requests.   
  2. Second, you must instantiate one of the server classes, passing it the server’s address and the request handler class.
  3. Then call the handle_request() or serve_forever() method of the server object to process one or many requests.
  4. Finally, call server_close() to close the socket.

最基本的socketserver代码实现

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            try:
                self.data = self.request.recv(1024).strip()
                print(self.data)
                self.request.send(self.data.upper())
            except Exception as e:
                print('链接断开',e)
                break

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
    server.serve_forever()

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

class socketserver.ForkingTCPServer

class socketserver.ForkingUDPServer

class socketserver.ThreadingTCPServer

class socketserver.ThreadingUDPServer

so 只需要把下面这句

server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

换成下面这个,就可以多并发了,这样,客户端每连进一个来,服务器端就会分配一个新的线程来处理这个客户端的请求

server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)

  当然,知道了socket的实现原理之后,我们还可以使用socket来完成很多事情,比如说用socket来完成一个FTP文件上传下载的功能也都可以。。。学无止境,慢慢加油吧。

posted @ 2018-12-09 16:05  WuSir_ZJ  阅读(155)  评论(0编辑  收藏  举报