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 socketserverimport json,osclass 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)                breakif __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 socketimport jsonimport 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()
时间: 2024-09-30 12:29:57

Python全栈开发-Day8-Socket网络编程的相关文章

python全栈开发-Day8 函数基础

python全栈开发-Day8 函数 一 .引子 1. 为何要用函数之不用函数的问题 #1.代码的组织结构不清晰,可读性差 #2.遇到重复的功能只能重复编写实现代码,代码冗余 #3.功能需要扩展时,需要找出所有实现该功能的地方修改之,无法统一管理且维护难度极大  2. 函数是什么? 针对二中的问题,想象生活中的例子,修理工需要实现准备好工具箱里面放好锤子,扳手,钳子等工具,然后遇到锤钉子的场景,拿来锤子用就可以,而无需临时再制造一把锤子. 修理工===>程序员 具备某一功能的工具===>函数

Python全栈开发-Day6-面向对象编程

本节内容: 面向过程VS面向对象 面向对象编程介绍 类的语法 构造函数.析构函数 私有方法.私有属性 面向对象的特性:封装.继承.多态 1.面向过程 VS 面向对象 编程范式 编程是程序员用特定的语法+数据结构+算法组成的代码来告诉计算机如何执行任务的过程,一个程序是程序员为了得到一个任务结果而编写的一组指令的集合,实现一个任务的方式有很多种不同的方式,对这些不同的编程方式的特点进行归纳总结得出来的编程方式类别,即为编程范式.不同的编程范式本质上代表对各种类型的任务采取的不同的解决问题的思路,大

39.Python全栈之路:网络编程

socket socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求.      

Python全栈开发day8

一.python生成/迭代器 yiled生成数据 python迭代器, 访问数据(通过next一次一次去取) 二.反射 通过字符串的形式,导入模块 通过字符串的形式,到模块中,寻找指定的函数,并执行 实质:以字符串的形式,到对象中,操作(更改,删除等)其方法(或叫做操作成员) __import__ 用于以字符串的形式寻找对象 getattr() 用于以字符串的形式去某个对象中寻找东西(函数,变量等) hasattr()  用于以字符串的形式去某个对象中判断东西(函数,变量等)是否存在,存在返回T

Python全栈开发-Day7-面向对象编程2

本节内容: 1.面向对象高级语法部分 1)静态方法.类方法.属性方法 3)类的特殊方法 4)反射 2.异常处理 静态方法 通过@staticmethod装饰器即可把其装饰的方法变为一个静态方法,什么是静态方法呢?其实不难理解,普通的方法,可以在实例化后直接调用,并且在方法里可以通过self.调用实例变量或类变量,但静态方法是不可以访问实例变量或类变量的,一个不能访问实例变量和类变量的方法,其实相当于跟类本身已经没什么关系了,它与类唯一的关联就是需要通过类名来调用这个方法 1 2 3 4 5 6

Python 全栈开发【第一篇】:目录

Python 全栈开发[第0篇]:目录 第一阶段:Python 开发入门 Python 全栈开发[第一篇]:计算机原理&Linux系统入门 Python 全栈开发[第二篇]:Python基础语法入门 Python 全栈开发[第三篇]:数据类型.字符编码.文件操作 第二阶段:函数编程&常用标准库 Python 全栈开发[第四篇]:函数.递归.生成器.迭代器 Pyhton 全栈开发[第五篇]:常用模块学习 第三阶段:面向对象编程&网络编程基础 Python 全栈开发[第六篇]:面向对象

python全栈开发学习目录

python全栈开发学习目录 第一章 计算机基础 第二章Python入门 第三章数据类型 第四章文件操作 第五章函数 第六章 模块 第七章 面向对象 第八章 网络编程 第九章 并发编程 第十章 数据库 第十一章 前端开发-html 第十一章 前端开发-css 附加:js特效 15个小demo 第十一章 前端开发-JavaScript 第十一章 前端开发-jQuery 第十一章 前端开发-bootstrap 第十二章 Django框架开发 ... 原文地址:https://www.cnblogs.

python全栈开发目录

python全栈开发目录 linux命令 初识python python基础数据类型 函数编程.set.深浅拷贝 内置函数 文件操作 装饰器 迭代器和生成器 常用模块 初识类和对象 类和对象(进阶) 反射 异常处理 socket.IO多路复用 线程.进程.协程 HTML CSS JavaScript DOM文档操作 jQuery实例 web框架本质 Tornado mysql基础 mysql进阶 ..... 基本算法 递归--二分法查找 冒泡排序 更多 线程池

Python全栈开发【基础四】

Python全栈开发[基础四] 本节内容: 匿名函数(lambda) 函数式编程(map,filter,reduce) 文件处理 匿名函数 lambda表达式:对于简单的函数,存在一种简便的表示方式,即lambda表达式 1 #这段代码 2 def calc(n): 3 return n**n 4 print(calc(10)) 5 6 #换成匿名函数 7 calc = lambda n:n**n 8 print(calc(10)) 匿名函数主要是和其它函数搭配使用 举例: 1 ########

自学Python全栈开发第一次笔记

我已经跟着视频自学好几天Python全栈开发了,今天决定听老师的,开始写blog,听说大神都回来写blog来记录自己的成长. 我特别认真的跟着这个视频来学习,(他们开课前的保证书,我也写了一份,哈哈哈...)我现在是准大学生,准备学习编程,日后做一个程序员,哈哈哈.听说程序员很苦逼,不过貌似挣得也很多啊.并且我貌似也只喜欢计算机这个方面,所以我想在这个行业发光. 前些天学习了一些Linux一些命令: pwd     查看你当前所在的目录  /root=计算机/E盘 /    是根目录 cd(ch