第八章 网络编程

8.1网络基础

相关概念:

  • 两个运行中的程序如何传递信息?

    • 通过文件

  • 两台机器上的两个运行中的程序如何通信?

    • 通过网络

  • 网络应用开发架构

    • C/S 迅雷 qq 浏览器 输入法 百度云 pycharm git 红蜘蛛

      • client 客户端

      • server 服务端

    • B /S 淘宝 邮箱 各种游戏 百度 博客园 知乎 豆瓣 抽屉(B/S 会成为主流)

      • browser 浏览器

      • server 服务端

    • 统一程序的入口

    • B/S 和C/S架构的关系

      • B/S是特殊的C/S的架构

基础概念:

  • 网卡:是一个实际存在计算机中的硬件

  • mac地址:每一块网卡上都有一个全球唯一的mac地址

  • 交换机:是连接多台机器并帮助通讯的物理设备,只认识mac地址

  • 协议:server和client得到的内容都是二进制,所以两台物理设备之间对于要发送的内容、长度、顺序做了一些约定

IP地址:

  • ipv4协议 位的点分十进制 32位2进制表示

  • 0.0.0.0 - 255.255.255.255

  • ipv6协议 6位的冒分十六进制 128位2进制表示

  • 0:0:0:0:0:0 - FFFF:FFFF:FFFF:FFFF:FFFF:FFFF

公网ip:

  • 为什么你的外地朋友的电脑我们访问不了?

    • 每一个ip地址要想被所有人访问到,那么这个ip地址必须是你申请的

内网ip(保留的)

  • 192.168.0.0 - 192.168.255.255

  • 172.16.0.0 - 172.31.255.255

  • 10.0.0.0 - 10.255.255.255

交换机实现的arp协议

  • 通过ip地址获取一台机器的mac地址

 

网关ip :一个局域网的网络出口,访问局域网之外的区域都需要经过路由器和网关

交换机进行局域网内的通讯,路由器进行局域网之间的通讯

 

网段: 指的是一个地址段x.x.x.0 x.x.0.0 x.0.0.0

 

子网掩码: 判断两台机器是否在同一个网段内的

255.255.255.0 子网掩码 24个1

11111111.11111111.11111111.00000000

 

ip地址能够确认一台机器

port 端口

  • 0 - 65535

  • 80

ip + port 确认一台机器上的一个应用

端口推荐使用8000以后的端口

8.2 网络架构

8.2.1C/S架构及其他相关内容

  • C/S client- server:一个服务器对多个用户

C 指的是client(客户端软件)S指的是server(服务端软件)

  • B/S browser - server

    • web服务

    • b/s是特殊的c/s架构

物理设备

  • 网卡:mac地址 全球唯一的物理地址

  • 交换机 : 完成局域网内的多台机器之间的通信

    • 单播、组播、广播

    • 只能识别mac地址

    • arp协议(地址解析协议):通过ip地址获取它的mac地址

      • 通过先广播在单播找到arp协议

      • 由交换机完成的

  • 路由器 :完成局域网与局域网之间的联系

    • 只能识别ip地址

    • 网段

    • 网关ip

      • 访问局域网外部服务的一个出口ip

  • 通过ip地址可以在网络上定位一台机器

    • ipv4

    • ipv6 128位的,6位冒分,16进制

  • 端口 port:能够在网络上定位一台机器上的一个服务

    • 范围: 0-65535

8.2.2 TCP/IP协议

中译名为传输控制协议/因特网互联协议,又名网络通讯协议,是Internet最基本的协议、Internet国际互联网络的基础

  • TCP 协议(三次握手、四次握手图)爬虫需要掌握这个

    • 协议的特点(可靠、慢):通信建立在全双工的链接的基础上。可靠是因为建立链接之后不会因为网络的抖动而接收不到,每发送一次信息都会收到对应的回执,为了保证数据的完整性,还有重传机制

    • 应用:邮件、文件、http web

    • 三次握手(链接的过程):信息在client端和server端传递了三次建立了链接(syn,ack)

      • server端是accept接收过程中建立起了三次握手

        • accept接受过程中等待客户端的连接

        • connect会发起一个syn链接请求

          • 如果得到了server端响应ack的同时还会在收到一个由server端发来的syc链接请求

          • client端进行回复ack之后,就建立起了一个tcp协议的链接

        • 三次握手的过程在代码中是由accept和connect共同完成的,具体细节在socket中没有体现出来

    • 四次握手(断开的过程):有收必有发,收发必相等,有多少个发送就有多少个接收,断开链接(client端发送请求断开,server通过并且server请求断开,client通过server发送的请求断开链接)(fin,ack)

      • server和client端对应的在代码中都有close方法

      • 每一端发起的close操作都是一次fin断开的f请求,得到‘断开确认ack’之后,就可以结束一端的数据发送,如果两端都发起close,那么就是两次请求和两次回复,一共是四次操作,可以结束两端的数据发送,表示连接断开了

    • 区别:三次握手把一个回复和请求连接的两条信息和并成一条了。而四次握手是因为由于一方断开链接之后,可能另一方还有暑假没有传递完,所以不能立即断开,所以挥手的时候不能合并信息

    • tcp协议是个长链接:会一直占用双方的端口

    • IO(input,output)操作,输入和输出是相对内存来说的

      • write send -- output

      • read recv --- input

  • UDP协议---发短信

    • 协议的特点:无连接的、速度快,可能会丢消息,能够传递的长度是有限的,是根据数据传递设备的设置有关系(一般数据长度设置1500字节)

    • 是一个面向数据报的,无连接的,不可靠,快的,能完成一对一,一对多,多对一,多对多的高效通讯协议

      • 即时聊天工具 视频在线观看

  • osi七层模型

    • 应用层

    • 表示层

    • 会话层

    • 传输层

    • 网络层

    • 数据链路层

    • 物理层

    • 每一层的物理设备

    • 每一层的常见协议

  • osi五层模型(每层都是虚拟的)---重点

    • 应用层(包含:表示层会话层) python代码、http、HTTPS、ftp、smtp协议

    • 传输层:包装tcp、udp、端口协议(物理设备:有四层路由器和四层交换机)

    • 网络层:ipv4/ipv6协议(物理设备:(三层)路由器)(三层交换机带有路由功能的交换机)

    • 数据链路层:mac地址、arp协议 (物理设备:网卡、(二层)交换机)

    • 物理层: 二进制

  • 应用场景:

    • TCP 文件的上传下载(发送邮件、网盘、缓存电影)

    • UDP 即时通信类的(qq、微信、飞秋)

8.3 socket介绍

8.3.1 什么是socket

socket是套接字

  • socket是python借助socket完成socket的功能

  • socket是工作在应用层和传输层之间的抽象层,帮助我们完成了所有信息的组织和拼接

  • socket对于程序员来说已经是网络操作的底层了

  • socket历史

    • 同一台机器上的两个服务之间的通信的

      • 基于文件

    • 基于网络的多台机器之间的多个服务通信

8.3.2 应用

socket实例类

socket.socket()

tcp中的应用:

import socket
#服务端
sk = socket.socket()
sk.blind(('127.0.0.1',9000))
sk.listen() #n 允许多少客户端等待
print('*'*20)
while True:
    conn,addr = sk.accept()
    while True:
        msg = conn.recv(1024).decode('utf-8')
        if msg.upper() == b'Q':
            break
        print(msg.decode('utf-8'))
        inp = input('>>>>')
        conn.send(inp.encode('utf-8'))
        if inp.upper() == 'Q':
            break

    conn.close()
sk.close

#客户端
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
while True:
    inp = input('>>>>')
    sk.send(inp.encode('utf-8'))
    if inp.upper() == 'Q':
        break
    msg = sk.recv(1024).decode('utf-8')
    if msg == b'Q':
        break
    print(msg)
 sk.close()

udp 中的应用,可以实现多人互聊

import socket
sk = socket.socket(type = socket.SOCK_DGRAM)
sk.bind(('127.0.0.1',9000))
while True:
    msg,clientaddr = sk.recvfrom(1024)
    print(msg.decode('utf-8'))
    msg = input('>>>').encode('utf-8')
    sk.sendto(msg,clientaddr)
 sk.close()


import socket
sk = socket.socket(type = socket.SOCK_DGRAM)
while True:
    inp = input('>>>>').encode('utf-8')
    sk.sendto(inp,('127.0.0.1',9000))
    ret = sk.recv(1024)
    print(ret)
    
 sk.close()

总结:socket在tcp和udp中的不同:

编码问题:

send/sendto:

str --encode(编码) --bytes

recv/recvfrom:

bytes --decode(编码)---str

 

8.4 粘包

粘包问题只存在于tcp协议中

8.4.1什么是粘包现象?

  • 发生在发送端的粘包

    • 由于两个数据的发送时间间隔短+数据的长度小,所以由tcp协议的优化机制将两条信息作为一条信息发送出去了,为了减少tcp协议中的‘确认收到’的网络延迟时间

  • 发生在接收端的粘包

    • 由于tcp协议中所传输的数据无边界,所以来不及接收的多条数据会接收方的内核的缓存端粘在一起

  • 粘包现象的本质:接收信息的边界不清晰

8.4.2解决粘包问题

  • 自定义协议

    • 首先发送报头

      • 报头长度为4个字节

      • 内容是即将发送的报文的字节长度

      • struct模块

        • pack:能够把所以的数字都固定的转换成4字节

    • 再发送报文

  • 自定义协议2

    • 我们专门用来做文件发送的协议

      • 先发送报头字典的字节长度

      • 在发送字典(字典中包含文件的名字、大小。。。。)

      • 再发送文件的内容

    实例:

    server端

import time
import socket

sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()

conn,_ = sk.accept()
time.sleep(0.1)
msg1 = conn.recv(1024)
print(msg1)
msg2 = conn.recv(1024)
print(msg2)
conn.close()
sk.close()

client端

import struct
import socket

sk = socket.socket()
sk.connect(('127.0.0.1',9000))

msg = b'hello'
byte_len = struct.pack('i',len(msg))
sk.send(byte_len)   #  1829137
sk.send(msg)        #  1829139
msg = b'world'
byte_len = struct.pack('i',len(msg))
sk.send(byte_len)
sk.send(msg)

sk.close()

8.5 非阻塞io模型

非阻塞模型就是就算用户不来也会一直执行

server端

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.setblocking(False)  ##非阻塞
sk.listen()

conn_l = []
del_l = []
while True:
    try:
        conn,addr = sk.accept()   # 阻塞,直到有一个客户端来连我
        print(conn)
        conn_l.append(conn)
    except BlockingIOError:
        for c in conn_l:
            try:
                msg = c.recv(1024).decode('utf-8')
                if not msg:
                    del_l.append(c)
                    continue
                print('-->',[msg])
                c.send(msg.upper().encode('utf-8'))
            except BlockingIOError:pass
        for c in del_l:
            conn_l.remove(c)
        del_l.clear()
sk.close()

client1端

import time
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
for i in range(30):
    sk.send(b'wusir')
    msg = sk.recv(1024)
    print(msg)
    time.sleep(0.2)
sk.close()

client2

import socket
import time
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
while True:
    sk.send(b'wusir')
    msg = sk.recv(1024)
    print(msg)
    time.sleep(0.2)
sk.close()

socket的非阻塞io模型 + io多路复用实现的

  • 虽然非阻塞提高了cpu的利用率,但是耗费cpu做了很多无用功

8.6 socket 的更多方法介绍

服务端套接字函数
s.bind()    绑定(主机,端口号)到套接字
s.listen()  开始TCP监听
s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数
s.connect()     主动初始化TCP服务器连接
s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv()            接收TCP数据
s.send()            发送TCP数据
s.sendall()         发送TCP数据
s.recvfrom()        接收UDP数据
s.sendto()          发送UDP数据
s.getpeername()     连接到当前套接字的远端的地址
s.getsockname()     当前套接字的地址
s.getsockopt()      返回指定套接字的参数
s.setsockopt()      设置指定套接字的参数
s.close()           关闭套接字

面向锁的套接字方法
s.setblocking()     设置套接字的阻塞与非阻塞模式
s.settimeout()      设置阻塞套接字操作的超时时间
s.gettimeout()      得到阻塞套接字操作的超时时间

面向文件的套接字的函数
s.fileno()          套接字的文件描述符
s.makefile()        创建一个与该套接字相关的文件

8.7 验证客户端链接的合法性

如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么利用hashlib/hmac+加盐的方式来实现

server端

import os 
import socket
import hashlib
def get_md5(secret_key,randseq): #加密
    md5 = hashlib.md5(secret_key)
    md5.update(randseq)
    res = md5.hexdigest()
    return res

def chat(conn):  #接收信息
    while True:
        msg = conn.recv(1024).decode('utf-8')
        print(msg)
        conn.send(msg.upper().encode('utf-8'))
        
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()

secret_key = b'alexsb' #秘钥
while True:
    conn,addr = sk.accept()
    randseq = os.urandom(32) #生成随机字符串
    conn.send(randseq)
    md5code = get_md5(secret_key,randseq)
    ret = conn.recv(32).decode('utf-8') #client计算的结果
    print(ret)
    if ret == md5code: #client的结果和自己计算的结果
        print('是合法的客户端')
        chat(conn)  #信息合法进去chat函数
        
     else:
        print('不是合法的客户端')
        conn.close()
  sk.close()

 

client端

秘钥的算法

import hashlib
import socket
import time

def get_md5(secret_key,randseq):
    md5 = hashlib.md5(secret_key)
    md5.update(randseq)
    res = md5.hexdigest()
    return res
def chat(sk):
    while True:
        sk.send(b'hello')
        msg = sk.recv(1024).decode('utf-8')
        print(msg)
        time.sleep(0.5)
        
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
secret_key = b'alexsb'
randseq = sk.recv(32)
md5code = get_md5(secret_key,randseq)

sk.send(md5code.encode('utf-8'))
chat(sk) #进去chat函数
sk.close()

hmac模块 秘钥

import os
import hmac
secret_key = b'alexsb'
random_seq = os.urandom(32)
hmac = hmac.new(secret_key,random_seq)
ret = hmac.digest()
return ret

8.8 socketserver模块

是socket是它的底层模块,直接实现tcp协议可并发的server端

import socketserver
class Myserver(socketserver.BaseRequestHandler):
    def handle(self): #等待接收,自动触发handle方法,并且self.request ==conn
                 print(self.request)
        while True:  
            msg = self.request.recv(1024).decode('utf-8')
            print(msg)
            self.request.send(msg.upper().encode('utf-8'))

server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),Myserver)
server.serve_forever()
import socket
import time
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
for i in range(3):
    sk.send(b'hello,yuan')
    msg = sk.recv(1024)
    print(msg)
    time.sleep(1)

sk.close()

8.9 例题

8.9.1文件的传输下载

server端

import os
import json
import struct
from socketserver import *

server_dir = r'E:\python\s21day30\文件传输下载优化\server_dic'  #地址

class Myserver(BaseRequestHandler):
    def my_server(self,encoding='utf-8'):
        '''
        按照协议接收字典
        :param encoding: 编码
        :return: 从网络上接收到的字典格式的结果
        '''
        dic_len = self.request.recv(4)
        dic_len = struct.unpack('i',dic_len)[0]
        dic = self.request.recv(dic_len).decode(encoding)
        dic = json.loads(dic)
        return dic
    def my_send(self,dic,encoding='utf-8'):
        '''
        按照协议发送字典
        :param dic: 要发送的字典内容
        :param encoding: 编码
        :return:
        '''
        str_dic = json.dumps(dic)
        bdic = str_dic.encode(encoding)
        dic_len = len(bdic)
        bytes_len = struct.pack('i',dic_len)
        self.request.send(bytes_len)
        self.request.send(bdic)

    def send_file(self,dic,filepath):
        '''
        发送文件
        :param dic: 文件信息的字典
        :param filepath: 待发送文件的所在路径
        :return:
        '''
        with open(filepath,'rb') as f:
            while dic['filesize'] >2048:
                content = f.read(2048)
                self.request.send(content)
                dic['filesize'] -= len(content)
            else:
                content = f.read()
                self.request.send(content)

    def download(self,dic):
        '''
        下载功能
        :param dic: 客户端发来的字典信息,包含要下载的文件名
        :return:
        '''
        filename = dic['filename'] #获取文件名
        filepath = os.path.join(server_dir,filename)#拼接绝对路径
        dic = {}
        if os.path.isfile(filepath):   #判断用户要下载的是否是个文件
            dic['filesize'] = os.path.getsize(filepath)   #获取文件的大小
            dic['isfile']  = True   #将信号设置为True,以便客户端判断文件是否可以执行下载逻辑
            self.my_send(dic)      #发送字典的信息
            self.send_file(dic,filepath)   #发送文件
        else:
            dic['isfile'] = False   #将信号设置为False,以便客户端判断文件是否可以执行下载逻辑
            self.my_send(dic)       #发送字典信息


    def handle(self):
        dic = self.my_recv()
        if dic['operate'] == 'download':
            self.download(dic)

server = ThreadingTCPServer(('127.0.0.1',9000),Myserver)
server.serve_forever()

client端

import os
import json
import socket
import struct

server_dir = r'E:\python\s21day30\文件传输下载优化\loacal_dir'
sk = socket.socket()
sk.connect(('127.0.0.1',9000))

def mysend(sk,dic,encoding='utf-8'):
    str_dic = json.dumps(dic)
    bdic= str_dic.encode(encoding)
    dic_len = len(bdic)
    bytes_len = struct.pack('i',dic_len)
    sk.send(bytes_len)
    sk.send(bdic)


def myrecv(dic,encoding='utf-8'):
    dic_len = sk.recv(4)   #接收要下载的文件信息
    dic_len = struct.unpack('i',dic_len)[0]
    dic = sk.recv(dic_len).decode(encoding)
    dic = json.loads(dic)
    return dic

def recv_file(filename,sk,dic,buffer = 2048):
    '''
    接收文件
    :param filename: 文件名
    :param sk: socket对象
    :param dic: 存的文件的大小等信息
    :param buffer: 默认一次收取的数据长度
    :return:
    '''
    def inner_recv(buffsize = buffer,recvsize = buffer):
        '''
        按照buffersize进行判断,接收recvsize长度的内容,并写文件
        :param buffsize:
        :param recvsize:
        :return:
        '''
        while dic['filesize']  > buffsize:
            content = sk.recv(recvsize)
            f.write(content)
            dic['filesize'] -= len(content)

    filepath = os.path.join(server_dir,filename)
    with open(filepath,'wb') as f:
        inner_recv()   #接收数据,每次接收2048个,直到剩余的内容小于等于2048
        inner_recv(0,dic['filesize'])   #接收剩余的2048个以内的字节,直到全部接收完

filename = input('>>>>').strip()
dic = {'filename':filename,'operate':'download'}
mysend(sk,dic)  #发送要下载的字典的信息
dic = myrecv()  #接收结果,内容包括是否能下载,以及要下载的文件大小等信息
if dic['isfile']:
    recv_file(filename,sk,dic)
else:
    print('您要下载的文件不存在')


filename = input('>>>>')
dic = {'filename':filename,'operate':'download'}

 

 

     

上一篇:新零售时代,我们如何提升线下业务的终端可用性?


下一篇:python各种单例模式