Python全栈开发-Day8-Socket网络编程

Posted GavinSimons

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python全栈开发-Day8-Socket网络编程相关的知识,希望对你有一定的参考价值。

本节内容

  1. 断言
  2. Socket构建框架
  3. ftp构建框架
  4. Socket粘包
  5. Socket介绍
  6. Socket参数介绍
  7. 基本Socket实例
  8. 通过Socket实现简单SSH
  9. SocketServer
  10. 支持多用户在线传输的FTP程序

 

1、断言

断言作用是,下面代码的执行要严格依据上面的执行结果,断言则为判断上面代码的结果是否符合下面代码执行的前提,有点类似于登机安检。

assert type(obj.name) is str

上面这句话就是断言,如果断言为真,则继续执行下面代码,如果为假,则报错,错误类别为断言错误,即asserterror。

 

2、Socket构建框架

由于面试时,很可能被要求书写Socket的简易客户端和服务端代码,所以现在归纳把框架归纳如下:

1)服务端:

  server = socket.socket()

  server.bind(localhost,9999)

  server.listen()

  while True: 

    conn,addr = server.accept()  #阻塞状态

    while True:

      data = conn.recv(1024)  #官方建议最大不超过8192=8k, recv默认是阻塞的。

      if not dat : break  #客户端一断开,conn.recv(1024)收到的都是空数据。

      conn.send(data)

  #这套代码,只能同时服务一个客户端

2)客户端

  client = socket.socket()

  client.connect(server_ip, port)

  client.send()

  client.recv(1024)

3)注意:

  send指令不能发送空数据,如果send要发送的数据为空,计算机不会执行发送任务,会报错,并且继续往下执行下面代码。

  send发送的规则:1缓冲区满一定会发2超时一定会发。

  当运用循环进行连续完整send和recv数据时,我们需要第一步用len函数判断需要传送数据的长度,便于服务器端进行判断循环多少次可以完全接受完整个数据流。所以在这种情况下,server端需要发送2次send,第一次是将要发送的数据长度,第二次是发送数据本身。client端接收数据时,也需要recv两次,分别对应server端发送的数据。在统计数据长度时,使用len函数进行统计,这块需要注意,len函数对汉字字符串的统计为单个汉字的长度为1,但是len函数对字符形式的单个汉字统计结果是单个汉字长度为3,所以在server端和client端统计数据长度时要统一,要不大家都统计数据的字符格式,要不大家同时统计数据的二进制格式,只有这样才能保证server端发送的数据长度正好等于client端接受的数据长度。

 

3、ftp构建框架:

1、server
  1)读取文件名
  2)检测文件是否存在
  3)打开文件
  4)检测文件大小
  5)向client发送文件大小
  6)等待客户端确认
  7)开始边读边发数据
  8)向client发送文件md5值

 2、注意,在server的框架中,还有一个地方有连续的两次send指令,即最后一次发送的文件数据和发送的md5值连续调用两次send,这里可能出现黏包现象。在这里的解决办法不需要让server端recv导致强制发送。由于此处client已经知道自己需要接收多大的数据流了,所以只让client接收这么多数据,后面的数据不接收了。然后用一条判断语句,如果总大小-已接收大小>1024,则这次接收接收1024,如果不满足上述条件,则最后这次接收接收总大小-已接收大小的数值。

 

4、Socket粘包

粘包的形成原因:连续两次调用send函数,缓冲区会把这两次send的数据当成一次,发给client端。

粘包不是每次都会黏,不知道何时缓冲区就会粘包一次。

解决方法:

  在server的两次send之间,插入一个recv命令,用来接收client发出的接到数据长度的回复。此时server端的recv是阻塞状态,之前的send会被强制发送。

 

5、Socket 介绍

Socket是对TCP/IP和UDP底层协议的封装,暴露出API供其他协议(https,ftp,dns等)使用。所以如果想自己写一个网络传输协议,至少需要学会Socket协议。Socket中只干两件事,发数据send和接受数据receive,至于细节可以自己定义。

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 

 

6. Socket 参数介绍

server = socket.socket(family=AF_INETtype=SOCK_STREAMproto=0fileno=None)  

server.bind(address) 

  server.bind(address) 绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。

server.listen(backlog) 

  开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。

      backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5
      这个值不能无限大,因为要在内核中维护连接队列

server.accept() 

  接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。

  接收TCP 客户的连接(阻塞式)等待连接的到来

server.connect(address) 

  连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。

server.close() 

  关闭套接字

server.recv(bufsize[,flag])

  接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。

server.send(string[,flag]) 

  将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。

server.sendall(string[,flag]) 

  将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。

      内部通过递归调用send,将所有内容发送出去。

 

7. 基本Socket实例

1)server服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import socket
 
server = socket.socket() #获得socket实例
 
server.bind(("localhost",9998)) #绑定ip port
server.listen()  #开始监听
 
while True#第一层loop
    print("等待客户端的连接...")
    conn,addr = server.accept() #接受并建立与客户端的连接,程序在此处开始阻塞,只到有客户端连接进来...
    print("新连接:",addr )
    while True:
 
        data = conn.recv(1024)
        if not data:
            print("客户端断开了...")
            break #这里断开就会再次回到第一次外层的loop
        print("收到消息:",data)
        conn.send(data.upper())
 
server.close()

2)client客户端

 1 import socket
 2 
 3 client = socket.socket()
 4 
 5 client.connect(("localhost",9998))
 6 
 7 while True:
 8     msg = input(">>:").strip()
 9     if len(msg) == 0:continue
10     client.send( msg.encode("utf-8") )
11 
12     data = client.recv(1024)
13     print("来自服务器:",data)
14 
15 client.close()

8.通过socket实现简单的ssh

使用socket可以做一个极简版的ssh,就是客户端连接上服务器后,让服务器执行命令,并返回结果给客户端。

客户端要循环接收服务器端的大量数据返回,直到一条命令的结果全部返回为止。但问题是客户端知道服务器端返回的数据有多大么?肯定是不知道的。所以只能让服务器在发送数据之前主动告诉客户端,要发送多少数据给客户端,然后再开始发送数据。

我们在客户端本想只接收服务器端即将发送的数据大小结果,但实际上却连数据本身也跟着接收了一部分。

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

 

我们必须想办法把粘包分开,方法如下:

  服务器端每发送一个数据给客户端,就立刻等待客户端进行回应,即调用 conn.recv(1024),由于recv在接收不到数据时是阻塞的,这样就会造成,服务器端接收不到客户端的响应,就不会执行后面的conn.sendall(命令结果)的指令,收到客户端响应后,再发送命令结果时,缓冲区就已经被清空了,因为上一次的数据已经被强制发到客户端了。代码如下:

 1)server

import socket
import os,subprocess

server = socket.socket() #获得socket实例
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

server.bind(("localhost",9999)) #绑定ip port
server.listen()  #开始监听

while True: #第一层loop
    print("等待客户端的连接...")
    conn,addr = server.accept() #接受并建立与客户端的连接,程序在此处开始阻塞,只到有客户端连接进来...
    print("新连接:",addr )
    while True:

        data = conn.recv(1024)
        if not data:
            print("客户端断开了...")
            break #这里断开就会再次回到第一次外层的loop
        print("收到命令:",data)
        #res = os.popen(data.decode()).read() #py3 里socket发送的只有bytes,os.popen又只能接受str,所以要decode一下
        res = subprocess.Popen(data,shell=True,stdout=subprocess.PIPE).stdout.read() #跟上面那条命令的效果是一样的
        if len(res) == 0:
            res = "cmd exec success,has not output!".encode("utf-8")
        conn.send(str(len(res)).encode("utf-8")) #发送数据之前,先告诉客户端要发多少数据给它
        print("等待客户ack应答...")
        client_final_ack = conn.recv(1024) #等待客户端响应
        print("客户应答:",client_final_ack.decode())
        print(type(res))
        conn.sendall(res) #发送端也有最大数据量限制,所以这里用sendall,相当于重复循环调用conn.send,直至数据发送完毕

server.close()

 2)client

import socket
import sys

client = socket.socket()

client.connect(("localhost",9999))

while True:
    msg = input(">>:").strip()
    if len(msg) == 0:continue
    client.send( msg.encode("utf-8") )

    res_return_size  = client.recv(1024) #接收这条命令执行结果的大小
    print("getting cmd result , ", res_return_size)
    total_rece_size = int(res_return_size)
    print("total size:",res_return_size)
    client.send("准备好接收了,发吧".encode("utf-8"))
    received_size = 0 #已接收到的数据
    cmd_res = b‘‘
    f = open("test_copy.html","wb")#把接收到的结果存下来
    while received_size != total_rece_size: #代表还没收完
        data = client.recv(1024)
        received_size += len(data) #为什么不是直接1024,还判断len干嘛,注意,实际收到的data有可能比1024少
        cmd_res += data
    else:
        print("数据收完了",received_size)
        #print(cmd_res.decode())
        f.write(cmd_res) #把接收到的结果存下来
    #print(data.decode()) #命令执行结果

client.close()

9. SocketServer

 SocketServer最主要的作用就是实现并发处理。SocketServer是对Socket的再封装。

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

上面这条指令是TCP的SocketServer

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

上面这条指令是UDP的SocketServer

 

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

  1. 首先,你需要创建一处理请求的类,并且要继承BaseRequestHandler,并且还要重写父类里的handle()方法。这个handle()方法将会处理进来的请求。
  2. 第二,你需要实例化一个TCPServer的对象,并且传递server ip和你上面创建的请求处理类,给这个TCPServer
  3. 第三,调用server.handle_request()#只处理一个请求,或者,server.serve_forever()#处理多个请求,永远执行
  4. 最后,调用server.close()来关闭这个socket

注意,与client的所有的交互都是在handle()方法中完成的。

基本的socketserver代码

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    """
    The request handler class for our server.

    It is instantiated once per connection to the server, and must
    override the handle() method to implement communication to the
    client.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        # just send back the same data, but upper-cased
        self.request.sendall(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    # Create the server, binding to localhost on port 9999
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

    # Activate the server; this will keep running until you
    # interrupt the program with Ctrl-C
    server.serve_forever()

但上面的代码,依然不能同时处理多个连接。想要实现socketserver的多并发,只需要把下面这句

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

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

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

 

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

class socketserver.ForkingTCPServer  #Forking是进程的意思,这个类是多进程并发处理TCP协议的链接,但是只能在linux系统上使用。

class socketserver.ForkingUDPServer  #Forking是进程的意思,这个类是多进程并发处理UDP协议的链接,但是只能在linux系统上使用。

class socketserver.ThreadingTCPServer  #Threading是线程的意思,这个类是多线程并发处理TCP协议的链接

class socketserver.ThreadingUDPServer  #Threading是线程的意思,这个类是多线程并发处理UDP协议的链接

只需要把之前修改的类换成对应的上面的类,就可以实现多进程或多线程并发处理TCP协议或UDP协议的链接

 

class socketserver.BaseServer(server_addressRequestHandlerClass) 主要有以下方法

class socketserver.BaseServer(server_address, RequestHandlerClass)
This is the superclass of all Server objects in the module. It defines the interface, given below, but does not implement most of the methods, which is done in subclasses. The two parameters are stored in the respective server_address and RequestHandlerClass attributes.

fileno()  #返回文件描述符,一般用不到,是系统内部调用时用的。
Return an integer file descriptor for the socket on which the server is listening. This function is most commonly passed to selectors, to allow monitoring multiple servers in the same process.

handle_request()  #处理单个链接请求,我们一般也不用。
Process a single request. This function calls the following methods in order: get_request(), verify_request(), and process_request(). If the user-provided handle() method of the handler class raises an exception, the server’s handle_error() method will be called. If no request is received within timeout seconds, handle_timeout() will be called and handle_request() will return.

10、支持多用户在线传输的FTP程序

1)server

import socketserver
import json,os
class MyTCPHandler(socketserver.BaseRequestHandler):

def put(self,*args):
‘‘‘接收客户端文件‘‘‘
cmd_dic = args[0]
filename = cmd_dic["filename"]
filesize = cmd_dic["size"]
if os.path.isfile(filename):
f = open(filename + ".new","wb")
else:
f = open(filename , "wb")

self.request.send(b"200 ok")
received_size = 0
while received_size < filesize:
data = self.request.recv(1024)
f.write(data)
received_size += len(data)
else:
print("file [%s] has uploaded..." % filename)

def handle(self):
while True:
try:
self.data = self.request.recv(1024).strip()
print("{} wrote:".format(self.client_address[0]))
print(self.data)
cmd_dic = json.loads(self.data.decode())
action = cmd_dic["action"]
if hasattr(self,action):
func = getattr(self,action)
func(cmd_dic)

except ConnectionResetError as e:
print("err",e)
break
if __name__ == "__main__":
HOST, PORT = "localhost", 9999
# Create the server, binding to localhost on port 9999
server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
server.serve_forever()

2)client

import socket
import json
import os
#client.connect((‘192.168.16.200‘,9999))

class FtpClient(object):
def __init__(self):
self.client = socket.socket()
def help(self):
msg = ‘‘‘
ls
pwd
cd ../..
get filename
put filename
‘‘‘
print(msg)
def connect(self,ip,port):
self.client.connect((ip, port))
def interactive(self):
#self.authenticate()
while True:
cmd = input(">>").strip()
if len(cmd) ==0:continue
cmd_str = cmd.split()[0]
if hasattr(self,"cmd_%s" % cmd_str):
func = getattr(self,"cmd_%s" % cmd_str)
func(cmd)
else:
self.help()
def cmd_put(self,*args):
cmd_split = args[0].split()
if len(cmd_split) >1:
filename = cmd_split[1]
if os.path.isfile(filename):
filesize = os.stat(filename).st_size
msg_dic = {
"action": "put",
"filename":filename,
"size": filesize,
"overridden":True
}
self.client.send( json.dumps(msg_dic).encode("utf-8") )
print("send",json.dumps(msg_dic).encode("utf-8") )
#防止粘包,等服务器确认
server_response = self.client.recv(1024)
f = open(filename,"rb")
for line in f:
self.client.send(line)
else:
print("file upload success...")
f.close()

else:
print(filename,"is not exist")
def cmd_get(self):
pass


ftp = FtpClient()
ftp.connect("localhost",9999)
ftp.interactive()




















































































































以上是关于Python全栈开发-Day8-Socket网络编程的主要内容,如果未能解决你的问题,请参考以下文章

python全栈和python自动化课程的区别在哪?

python基础学习日志day8-socket上传文件

Python全栈开发之网络编程

Python全栈开发--网络编程一

python基础学习日志day8-socket发送大数据包问题

PYTHON高级全栈开发工程师-老男孩教育