第六章|网络编程-socket开发

1、计算机基础

作为应用开发程序员,我们开发的软件都是应用软件,而应用软件必须运行于操作系统之上,操作系统则运行于硬件之上,应用软件是无法直接操作硬件的,应用软件对硬件的操作必须调用操作系统的接口,由操作系统操控硬件。

比如客户端软件想要基于网络发送一条消息给服务端软件,流程是:

1、客户端软件产生数据,存放于客户端软件的内存中,然后调用接口将自己内存中的数据发送/拷贝给操作系统内存

2、客户端操作系统收到数据后,按照客户端软件指定的规则(即协议)、调用网卡发送数据

3、网络传输数据

4、服务端软件调用系统接口,想要将数据从操作系统内存拷贝到自己的内存中

5、服务端操作系统收到4的指令后,使用与客户端相同的规则(即协议)从网卡接收到数据,然后拷贝给服务端软件

TCP流失协议

 

  互联网协议就是计算机界的英语,网络就是物理链接介质+互联网协议。 我们需要做的是,让全世界的计算机都学会互联网协议,这样任意一台计算机在发消息时都严格按照协议规定的格式去组织数据,接收方就可以按照相同的协议解析出结果了,这就实现了全世界的计算机都能无障碍通信。 按照功能不同,人们将互联网协议分为osi七层或tcp/ip五层或tcp/ip四层(我们只需要掌握tcp/ip五层协议即可)

 

什么是TCP/IP?

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

OSI七层模型

 OSI/RM模型(Open System Interconnection / Reference Model)的设计目的是成为一个所有计算机厂商都能实现的开放网络模型,来克服使用众多私有网络模型所带来的困难和低效性。

TCP/IP五层模型详解

我们将应用层,表示层,会话层并作应用层,从tcp/ip五层协议的角度来阐述每层的由来与功能,搞清楚了每层的主要协议就理解了整个互联网通信的原理。

用户感知到的只是最上面一层应用层,自上而下每层都依赖于下一层;每层都运行特定的协议,越往上越靠近用户,越往下越靠近硬件。

1、物理层功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0;

2、数据链路层由来:单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思

数据链路层的功能:定义了电信号的分组方式

以太网协议:

早期的时候各个公司都有自己的分组方式,后来形成了统一的标准,即以太网协议ethernet

Ethernet规定

  • 一组电信号构成一个数据包,叫做一组数据‘帧’;
  • 每一数据帧分成:报头head和数据data两部分。

 

head包含:(规定为18个字节)

  • 发送者/源地址,6个字节(源地址) 
  • 数据类型,6个字节 (对数据的描述信息)
  • 接收者/目标地址,6个字节(目标地址)

data包含:(最短46字节,最长1500字节)

  • 数据包的具体内容

head长度+data长度=最短64字节,最长1518字节,超过最大限制就分片发送。

mac地址:

head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址,每块网卡出厂时都被烧制上一个世界唯一的mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号)

但凡接入互联网的机器必须要有一块网卡,如你的网线。网卡之上必须有一个mac地址就是网卡上的地址,一出厂就有,前六位是厂商编号,后六位是流水线号,保证这个地址是独一无二的;head的前六个字节和最后六个字节都是mac地址。

以太网协议的工作方式:广播,吼;基于mac地址标示对方;把包送过去,对方给你解出来;

以太网协议基于mac地址的广播只能在局域网。

3、IP地址| 网络层 

每台机器配个ip地址;跟之前一样的,也是有一个IP头和data数据。

有了ethernet、mac地址、广播的发送方式,世界上的计算机就可以彼此通信了,问题是世界范围的互联网是由一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网的广播方式,那么一台机器发送的包全世界都会收到; 必须找出一种方法来区分哪些计算机属于同一广播域,哪些不是,如果是就采用广播的方式发送,如果不是,就采用路由的方式(向不同广播域/子网分发数据包),mac地址是无法区分的,它只跟厂商有关。网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址。

IP协议:

  • 规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示
  • 范围0.0.0.0-255.255.255.255
  • 一个ip地址通常写成四段十进制数,例:172.16.10.1

子网掩码

所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。比如,IP地址172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么子网络掩码就是11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0。

子网掩码是用来标识一个IP地址的哪些位是代表网络位,以及哪些位是代表主机位。子网掩码不能单独存在,它必须结合IP地址一起使用。子网掩码只有一个作用,就是将某个IP地址划分成网络地址和主机地址两部分。

这就像寄信,你给你的南方姑娘寄信,她肉身在厦门,详细地址是厦门鼓浪屿三街27号,那网络位就相当于城市,详细地址就是主机位,网络位帮你定位到城市,主机位帮你找到你的南方姑娘。 路由器通过子网掩码来确定哪些是网络位,哪些是主机位。

划分子网本质上就是借主机位到给网络位,每借一位主机位,这个网段的可分配主机就会越少,比如192.168.1.0/24可用主机255个,借一位变成192.168.1.0/25,那可用主机就从255-128=127个了(从最大的值开始借),再借一位192.168.1.0/26,那可用主机数就变成了255-(128+64)=63个啦。

 IP地址和mac地址就可以找到世界上一台独一无二的机器。IP找到子网,mac找到子网它在哪个位置。

网络层还有个ARP协议,把ip地址解析为mac地址。只需要知道IP地址就可以了,自动根据ip解析。

发包就是一个封包的过程

4、传输层

基于IP+端口就可以找到任何一个软件端。

如何标识这台主机上的应用程序呢?答案就是端口,端口即应用程序与网卡关联的编号。

传输层功能:建立端口到端口的通信

补充:端口范围0-65535,0-1023为系统占用端口

传输层有两种协议,TCP和UDP,见下图

tcp协议

可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。

为什么tcp是可靠的数据传输呢?

最可靠的方式就是只要不得到确认,就重新发送数据报,直到得到对方的确认为止。

tcp的3次握手和4四挥手

udp协议

不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。

 2、socket

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。

基于socket实现简单套接字通信

#服务端有2种套接,
import socket
#1 买手机  #socket模块下的socket类,传了两个参数实例化一个对象,即phone套接字对象
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #套接字类型,用的流式的协议就是TCP协议;基于网络通信的TCP协议;family=socket.AF_INET地址家族,socket的类型;
#print(phone) #<socket.socket fd=208, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
#2 绑定手机卡
phone.bind(('127.0.0.1',8081)) #本地机,用来测试;要以元组形式传进来,第一个参数字符串形式的ip地址;端口的范围是0--65535,其中0-1024给操作系统使用
#3 开机
phone.listen(5)  #最大挂起连接数  #监听
#4 等待电话连接
print('start....')      #accept 和客户端的connect就是做的3次握手的事情;拿到conn既可以收又可以发消息,都是bytes格式
##phone.accept()的结果是一个元组的形式,两个数值。conn相当于拿到了电话线路;打印:(<socket.socket fd=216, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8081), raddr=('127.0.0.1', 52682)>, ('127.0.0.1', 52682)) 
conn,client_addr
= phone.accept() #把值取出来 相当于电话线路;一种是accept建套接;另一种是conn负责收发消息 #5 收消息发消息 conn 收发的结果都是bytes格式 data = conn.recv(1024) #收1024个字节,代表接收数据的最大数;1单位bytes 2. 1024代表最大接收1024bytes print('客户端的数据',data) conn.send(data.upper()) #给客户端回过去 #6 挂电话 conn.close() #7 关机 phone.close()
#客户端只有一种套接
import socket
#1 买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #套接字类型,用的流式的协议就是TCP协议;基于网络通信的TCP协议;#客户端的phone就是电话线,相当于服务端的conn
#print(phone) #<socket.socket fd=208, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
#2 拨号
phone.connect(('127.0.0.1',8081)) #本地机,用来测试;客户端只有一种套接,用来接收套接并收发消息
#3 发、收消息  phone这个对象既可以收又可以发消息
phone.send('hello'.encode('utf-8')) #要转换成二进制bytes类型,不能直接发字符串
data = phone.recv(1024)
#print(phone) #<socket.socket fd=208, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 49894), raddr=('127.0.0.1', 8081)> print(data) #4关闭 phone.close()

在简单套接字基础上(C/S)加上通信循环

import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',8081)) ##0-65535:0-1024给操作系统使用
phone.listen(5)
print('start....')
conn,client_addr = phone.accept()
print(client_addr) #打印ip端口
while True:  #通信循环
    data = conn.recv(1024)
    print('客户端的数据',data)
    conn.send(data.upper())
conn.close()
phone.close()
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8081))
while True:
    msg = input('>>>').strip()
    phone.send(msg.encode('utf-8'))
    data = phone.recv(1024)
    print(data)
phone.close()

修复小bug

 是否能发空消息呢?

import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) ##表示重用端口,在bind前加
phone.bind(('127.0.0.1',8081))
phone.listen(5)
print('start....')
conn,client_addr = phone.accept()
print(client_addr)
while True:
    data = conn.recv(1024)
    print('客户端的数据',data)
    conn.send(data.upper())
conn.close()
phone.close()

################修复下

import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #重用端口,在bind前加
phone.bind(('127.0.0.1',8081))
phone.listen(5)
print('start....')
conn,client_addr = phone.accept()
print(client_addr)
while True:
    try:
        data = conn.recv(1024)  #发空根本就收不到
        if not data:break #适用于linux操作系统 #windows应该用try except...
        print('客户端的数据',data)
        conn.send(data.upper())
    except ConnectionRefusedError: #适用于windows
        break
conn.close()
phone.close()
import socket
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8081))
while True:
    msg = input('>>>').strip()  #看看能不能收发空的,敲回车 #msg= ''
    if not msg:continue  #这样就解决了不能发空的问题
    phone.send(msg.encode('utf-8')) #phone.send(b'')
    print('has send')
    data = phone.recv(1024)
    print('has recv')  #发空的服务端没有回消息,就卡这了,所以说不能发空数据
    print(data)
    print(data.decode('utf-8')) #解码下
phone.close()

实现服务器可以对多个客户端提供服务

服务端一直提供服务;并发(以后学)

保证了客户端一个一个服务;链接循环一个一个服务

##服务端
import socket phone
=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',8083)) #0-65535:0-1024给操作系统使用 phone.listen(5) print('starting...') while True: # 链接循环 是一种折中的方案,一个一个服务 conn,client_addr=phone.accept() print(client_addr) while True: #通信循环 基于一个刚刚建好的链接 try: data=conn.recv(1024) if not data:break #适用于linux操作系统 print('客户端的数据',data) conn.send(data.upper()) except ConnectionResetError: #适用于windows操作系统 break conn.close()#关链接,关闭一次循环,然后再进入下一次 phone.close()
##多个客户端,代码一样
import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8083))
while True:
    msg=input('>>: ').strip() #msg=''
    if not msg:continue
    phone.send(msg.encode('utf-8')) #phone.send(b'')
    # print('has send')i
    data=phone.recv(1024)
    # print('has recv')
    print(data.decode('utf-8'))
phone.close()

 模拟ssh远程执行命令

  我们来写一个远程执行命令的程序,写一个socket client端在windows端发送指令,一个socket server在Linux端执行命令并返回结果给客户端

执行命令的话,肯定是用我们学过的subprocess模块啦,但注意注意注意:

res = subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdout=subprocess.PIPE)

命令结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码,且只能从管道里读一次结果。

#windows
#dir:查看某一个文件夹下的子文件名与子文件夹名
#ipconfig:查看本地网卡的ip信息
#tasklist:查看运行的进程


#linux:
#ls
#ifconfig
#ps aux



##执行系统命令,并且拿到命令的结果
# import os
# res=os.system('dir')
# print('命令的结果是:',res)

import subprocess
obj=subprocess.Popen('xxxdir',shell=True, #把命令结果丢到管道里边
                 stdout=subprocess.PIPE, #丢的属性,实际上触发的是功能的执行;正确管道
                 stderr=subprocess.PIPE) #错误的结果往这里边丢;
print(obj) #obj就是一个对象                               ##打印出: <subprocess.Popen object at 0x00000000021DF668>
print('stdout 1--->: ',obj.stdout.read().decode('gbk')) #第一个结果; linux用utf-8,windows用gbk;  打印出正确执行结果的信息。
#print('stdout 2--->: ',obj.stdout.read().decode('gbk')) #如果是正确命令,就在正确管道里边输出了,不会在错误管道里边输出

print('stderr 1--->: ',obj.stderr.read().decode('gbk'))  #打印出错误的信息:stderr 1--->:  'xxxdir' 不是内部或外部命令,也不是可运行的程序  或批处理文件
import socket
import subprocess

phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1',9901)) #0-65535:0-1024给操作系统使用
phone.listen(5)
print('starting...')
while True: # 链接循环
    conn,client_addr=phone.accept()
    print(client_addr)
    while True: #通信循环
        try:
            #1、收命令
            cmd=conn.recv(1024)
            if not cmd:break #适用于linux操作系统
            #2、执行命令,拿到结果
            obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout=obj.stdout.read()  #它就是bytes格式;正确的
            stderr=obj.stderr.read() #错误的结果
            #3、把命令的结果返回给客户端
            print(len(stdout)+len(stderr))
            conn.send(stdout+stderr) ### +是一个可以优化的点,申请了一个新的内存空间;

        except ConnectionResetError: #适用于windows操作系统
            break
    conn.close()

phone.close()
import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',9901))
while True:
    #1、发命令
    cmd=input('>>: ').strip() #ls /etc
    if not cmd:continue
    phone.send(cmd.encode('utf-8'))

    #2、拿命令的结果,并打印
    data=phone.recv(1024) #1024是一个坑,有可能发的命令结果大于1024个字节
    print(data.decode('gbk'))

phone.close()

3、粘包现象与解决方案

  top命令的结果比较长,但客户端只recv(1024), 可结果比1024长呀,那怎么办,只好在服务器端的IO缓冲区里把客户端还没收走的暂时存下来,等客户端下次再来收,所以当客户端第2次调用recv(1024)就会首先把上次没收完的数据先收下来,再收df命令的结果。 这个现象叫做粘包,就是指两次结果粘到一起了。它的发生主要是因为socket缓冲区导致的,来看一下:

  你的程序实际上无权直接操作网卡的,你操作网卡都是通过操作系统给用户程序暴露出来的接口,那每次你的程序要给远程发数据时,其实是先把数据从用户态copy到内核态,这样的操作是耗资源和时间的,频繁的在内核态和用户态之前交换数据势必会导致发送效率降低, 因此socket 为提高传输效率,发送方往往要收集到足够多的数据后才发送一次数据给对方。若连续几次需要send的数据都很少,通常TCP socket 会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

 TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。

粘包现象分析

   数据量小、间隔时间短的情况下才会发生粘包;客户端、服务端都可以粘包;

  1:不管是recv还是send都不是直接接收对方的数据,而是操作自己的操作系统内存--->不是一个send对应一个recv
  2:recv:(等待、拷贝数据)recv本质就是控制操作系统调网卡把数据收过来。
    wait data 耗时非常长;( 数据由对方一直发过来到我的操作系统缓存这是第一步;)
    copy data (由操作系统内存copy到应用程序内存,本地copy的过程。)
   send:      (本地copy数据的过程)
    copy data 

###服务端
import socket import time server
=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',9904)) #0-65535:0-1024给操作系统使用 server.listen(5) conn,addr=server.accept() #建链接 #第一次接收:5 # res1=conn.recv(1) #b'hello' # res2=conn.recv(1) #b'hello'##服务端粘包了 # res3=conn.recv(1) #b'hello' # res4=conn.recv(1) #b'hello' # res5=conn.recv(1) #b'hello' # print('第一次',res1+res2+res3+res4+res5) #b'h' res1=conn.recv(5) #world print(res1) time.sleep(6) #第二次接收:5 res1=conn.recv(2) #world res2=conn.recv(3) #world print('第二次',res1+res2)
import socket
import time

client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

client.connect(('127.0.0.1',9904))

client.send('hello'.encode('utf-8'))
time.sleep(5)
client.send('world'.encode('utf-8'))


##服务器打印:

  b'hello'
  第二次 b'world'

总结

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
  2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即s面向消息的通信是有消息保护边界的。
  3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略

 解决粘包问题

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

struct()的使用

import struct

res=struct.pack('i',1235) #i代表整型
print(res,type(res),len(res)) #res是个bytes类型;统计数据长度。把数字转成固定长度的bytes类型,就可以直接用于send传输了

#client.recv(4) #套接字,收到结果
obj=struct.unpack('i',res) #解包
print(obj) print(obj[
0]) #元组的形式,取0得到包头里边的数据

##打印:

  b'xd3x04x00x00'   <class 'bytes'>   4
  (1235,)

   1235

 ‘i’format requires -2147483648 <=number <=2147483647

简单版本的解决

##客户端
import socket import
struct phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',9909)) while True: #1、发命令 cmd=input('>>: ').strip() #ls /etc if not cmd:continue phone.send(cmd.encode('utf-8')) #2、拿命令的结果,并打印 #第一步:先收报头(先拿到数据的长度) header=phone.recv(4) #只收4个,收报头 #第二步:从报头中解析出对真实数据的描述信息(数据的长度) total_size=struct.unpack('i',header)[0] #反解,i格式,从报头从拿出对我有用的信息 #第三步:接收真实的数据 ##循环来取 recv_size=0 recv_data=b'' #拼接操作,接收的都是bytes类型 while recv_size < total_size: ##收完while循环才会让你结束进入下次循环输入input res=phone.recv(1024) #1024是一个坑 recv_data+=res ###每收一次就做一次拼接; recv_size+=len(res) ##+收的数据长度 print(recv_data.decode('gbk')) phone.close()
##服务端
import socket import subprocess import
struct phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',9909)) #0-65535:0-1024给操作系统使用 phone.listen(5) print('starting...') while True: # 链接循环 conn,client_addr=phone.accept() print(client_addr) while True: #通信循环 try: #1、收命令 cmd=conn.recv(8096) if not cmd:break #适用于linux操作系统 #2、执行命令,拿到结果 obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=obj.stdout.read() stderr=obj.stderr.read() #3、把命令的结果返回给客户端 #第一步:制作固定长度的报头 真实数据的信息 #一定得是固定长度,这样接收端才知道接收多少个数据,才能提取出报头来,才能提取出有用的信息来 total_size = len(stdout) + len(stderr) #数据的长度 header=struct.pack('i',total_size) #制作报头 i就是4个bytes #第二步:把报发送给客户端 ###三个send就是三个包,有可能会粘在一起;头要是固定标准固定长度,因为即使粘到一起了,我接收固定的长度,粘包也影响不到了 conn.send(header) #header本身就是bytes类型 #第三步:再发送真实的数据 #有头有数据才是互联网协议 conn.send(stdout) ##省去了那个+号,因为它们本身会产生粘包,优化出来了 conn.send(stderr) except ConnectionResetError: #适用于windows操作系统 break conn.close() phone.close()

  这个解决方案只包含数据的长度,报头应该是对真实数据的描述信息,报头不仅包含数据长度的信息;命令的长度可能超过这个范围,用i模式就不行了;

终极版的解决

####小知识点
import struct import json header_dic = { 'filename': 'a.txt', 'md5': 'xxdxxx', 'total_size': 33333333333333123123123123123333333333234239487239047902384729038479023874902387409237848902374902837490238749082374908237492837498023749082374902374890237498237492837409237409237402397420398749203742093749230749023874902387492083749023874029837420893479072839048723980472390874 } header_json = json.dumps(header_dic) # print(type(header_json)) #<class 'str'> 字符串类型 header_bytes=header_json.encode('utf-8') #转成bytes格式 # print(type(header_bytes)) #<class 'bytes'> #print(len(header_bytes)) #332个数据 struct.pack('i',len(header_bytes)) #报头的长度给它打成4个长度
##服务端
import subprocess import
struct import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',9909)) #0-65535:0-1024给操作系统使用 phone.listen(5) print('starting...') while True: # 链接循环 conn,client_addr=phone.accept() print(client_addr) while True: #通信循环 try: #1、收命令 cmd=conn.recv(8096) if not cmd:break #适用于linux操作系统 #2、执行命令,拿到结果 obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=obj.stdout.read() stderr=obj.stderr.read() #3、把命令的结果返回给客户端 #第一步:制作固定长度的报头 header_dic={ 'filename':'a.txt', 'md5':'xxdxxx', 'total_size': len(stdout) + len(stderr) } #字典不能直接转成bytes类型,可以转成字符串;反解出字典类型json header_json=json.dumps(header_dic) #json格式的字符串 header_bytes=header_json.encode('utf-8') #bytes类型 #第二步:先发送报头的长度 conn.send(struct.pack('i',len(header_bytes))) #4个bytes #第三步:再发报头 conn.send(header_bytes) #第四步:再发送真实的数据 conn.send(stdout) conn.send(stderr) except ConnectionResetError: #适用于windows操作系统 break conn.close() phone.close()
##客户端
import socket import
struct import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',9909)) while True: #1、发命令 cmd=input('>>: ').strip() #ls /etc if not cmd:continue phone.send(cmd.encode('utf-8')) #2、拿命令的结果,并打印 #第一步:先收报头的长度 obj=phone.recv(4) header_size=struct.unpack('i',obj)[0] #第二步:再收报头 header_bytes=phone.recv(header_size) #第三步:从报头中解析出对真实数据的描述信息 header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) total_size=header_dic['total_size'] #第四步:接收真实的数据 recv_size=0 recv_data=b'' while recv_size < total_size: res=phone.recv(1024) #1024是一个坑 recv_data+=res recv_size+=len(res) print(recv_data.decode('gbk')) phone.close()

4、文件传输

下载功能是服务端以读的方式打开文件;客户端以写的方式打开文件;

上传功能恰相反,客户端以读的方式打开,服务端以写的方式打开一个新文件接收客户端的传输;

下载功能的实现:

简单版的实现

#服务端
import socket
import subprocess
import struct
import json
import os

share_dir=r'C:UsersAdministratorPycharmProjectsmyFirstprochapter6网络编程文件传输简单版本servershare'

phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1',8912)) #0-65535:0-1024给操作系统使用
phone.listen(5)

print('starting...')
while True: # 链接循环
    conn,client_addr=phone.accept()
    print(client_addr)
    while True: #通信循环
        try:
            #1、收命令
            res=conn.recv(8096) # b'get 3.jpeg'
            if not res:break #适用于linux操作系统
            #2、解析命令,提取相应命令参数
            cmds=res.decode('utf-8').split() #['get','3.jpeg']
            filename=cmds[1]
            #3、以读的方式打开文件,读取文件内容发送给客户端
            #第一步:制作固定长度的报头
            header_dic={
                'filename': filename, #'filename':'3.jpeg'
                'md5':'xxdxxx',
                'file_size': os.path.getsize(r'%s/%s' %(share_dir,filename)) #os.path.getsize(r'C:UsersAdministratorPycharmProjectsmyFirstprochapter6网络编程文件传输简单版本servershare3.jpeg')
            }

            header_json=json.dumps(header_dic)
            header_bytes=header_json.encode('utf-8')
            #第二步:先发送报头的长度
            conn.send(struct.pack('i',len(header_bytes)))
            #第三步:再发报头
            conn.send(header_bytes)
            #第四步:再发送真实的数据
            with open('%s/%s' %(share_dir,filename),'rb') as f:
                # conn.send(f.read())
                for line in f:
                    conn.send(line)
        except ConnectionResetError: #适用于windows操作系统
            break
    conn.close()
phone.close()
##客户端
import socket
import struct
import json
download_dir=r'C:UsersAdministratorPycharmProjectsmyFirstprochapter6网络编程文件传输简单版本clientdownload'
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8912))
while True:
    #1、发命令
    cmd=input('>>: ').strip() #get a.txt
    if not cmd:continue
    phone.send(cmd.encode('utf-8'))
    #2、以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户的新文件
    #第一步:先收报头的长度
    obj=phone.recv(4)
    header_size=struct.unpack('i',obj)[0]
    #第二步:再收报头
    header_bytes=phone.recv(header_size)
    #第三步:从报头中解析出对真实数据的描述信息
    header_json=header_bytes.decode('utf-8')
    header_dic=json.loads(header_json)
    '''
            header_dic={
                'filename': filename, ##'filename':'3.jpeg'
                'md5':'xxdxxx',
                'file_size': os.path.getsize(filename)
            }
    '''
    print(header_dic)
    total_size=header_dic['file_size']
    filename=header_dic['filename']
    #第四步:接收真实的数据
    with open('%s/%s' %(download_dir,filename),'wb') as f:  #拼接一个绝对路径
        recv_size=0
        while recv_size < total_size:
            line=phone.recv(1024) #1024是一个坑
            f.write(line)
            recv_size+=len(line)
            print('总大小:%s   已下载大小:%s' %(total_size,recv_size))
phone.close()

使用函数进行代码的优化(下载)

#服务端
import socket
import subprocess
import struct
import json
import os
##都是全局变量
share_dir=r'C:UsersAdministratorPycharmProjectsmyFirstprochapter6网络编程文件传输优化版本servershare'
def get(conn,cmds):
    filename = cmds[1]
    # 3、以读的方式打开文件,读取文件内容发送给客户端
    # 第一步:制作固定长度的报头
    header_dic = {
        'filename': filename,  # 'filename':'3.jpeg'
        'md5': 'xxdxxx',
        'file_size': os.path.getsize(r'%s/%s' % (share_dir, filename))
    # C:UsersAdministratorPycharmProjectsmyFirstprochapter6网络编程文件传输优化版本servershare3.jpeg')
    }
    header_json = json.dumps(header_dic)
    header_bytes = header_json.encode('utf-8')
    # 第二步:先发送报头的长度
    conn.send(struct.pack('i', len(header_bytes)))
    # 第三步:再发报头
    conn.send(header_bytes)
    # 第四步:再发送真实的数据
    with open('%s/%s' % (share_dir, filename), 'rb') as f:
        # conn.send(f.read())
        for line in f:
            conn.send(line)
def put(conn,cmds):
    pass
def run():
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    phone.bind(('127.0.0.1',8912)) #0-65535:0-1024给操作系统使用
    phone.listen(5)
    print('starting...')
    while True: # 链接循环
        conn,client_addr=phone.accept()
        print(client_addr)
        while True: #通信循环
            try:
                #1、收命令
                res=conn.recv(8096) # b'put 1.mp4'
                if not res:break #适用于linux操作系统
                #2、解析命令,提取相应命令参数
                cmds=res.decode('utf-8').split() #['put','1.mp4']
                if cmds[0] == 'get':
                    get(conn,cmds)
                elif cmds[0] == 'put':
                    input(conn,cmds)
            except ConnectionResetError: #适用于windows操作系统
                break
        conn.close()
    phone.close()
if __name__ == '__main__':
    run()
#客户端
import socket
import struct
import json

download_dir=r'C:UsersAdministratorPycharmProjectsmyFirstprochapter6网络编程文件传输优化版本clientdownload'

def get(phone,cmds):
    # 2、以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户的新文件
    # 第一步:先收报头的长度
    obj = phone.recv(4)
    header_size = struct.unpack('i', obj)[0]
    # 第二步:再收报头
    header_bytes = phone.recv(header_size)
    # 第三步:从报头中解析出对真实数据的描述信息
    header_json = header_bytes.decode('utf-8')
    header_dic = json.loads(header_json)
    '''
            header_dic={
                'filename': filename, #'filename':'3.jpeg'
                'md5':'xxdxxx',
                'file_size': os.path.getsize(filename)
            }
    '''
    print(header_dic)
    total_size = header_dic['file_size']
    filename = header_dic['filename']
    # 第四步:接收真实的数据
    with open('%s/%s' % (download_dir, filename), 'wb') as f:
        recv_size = 0
        while recv_size < total_size:
            line = phone.recv(1024)  # 1024是一个坑
            f.write(line)
            recv_size += len(line)
            print('总大小:%s   已下载大小:%s' % (total_size, recv_size))
def put(phone,cmds):
    pass
def run():
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.connect(('127.0.0.1',8912))
    while True:
        #1、发命令
        inp=input('>>: ').strip() #get a.txt
        if not inp:continue
        phone.send(inp.encode('utf-8'))
        cmds=inp.split() #['get','a.txt']
        if cmds[0] == 'get':
            get(phone,cmds)
        elif cmds[0] == 'put':
            put(phone,cmds)
    phone.close()
if __name__ == '__main__':
    run()

基于面向对象的文件上传和下载

###服务端
import socket
import os
import struct
import pickle

class TCPServer:
    address_family = socket.AF_INET
    socket_type = socket.SOCK_STREAM
    listen_count = 5
    max_recv_bytes = 8192
    coding = 'utf-8'
    allow_reuse_address = False  #默认不允许重用端口
    # 下载的文件存放路径
    down_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'share')
    # 上传的文件存放路径
    upload_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'upload')

    def __init__(self,server_address,bind_and_listen=True):
        self.server_address = server_address
        self.socket = socket.socket(self.address_family,self.socket_type)

        if bind_and_listen:
            try:
                self.server_bind() #绑定
                self.server_listen()  #激活
            except Exception:
                self.server_close()

    def server_bind(self):
        if self.allow_reuse_address:
            self.socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
        self.socket.bind(self.server_address)
        #self.server_address = self.socket.getsockname()

    def server_listen(self):
        self.socket.listen(self.listen_count)

    def server_close(self):
        self.socket.close()

    def server_accept(self):
        return self.socket.accept()

    def conn_close(self,conn):
        conn.close()

    def run(self):
        print('starting...')
        while True:
            self.conn,self.client_addr = self.server_accept()
            print(self.client_addr)
            while True:
                try:
                    res = self.conn.recv(self.max_recv_bytes)
                    if not res:continue
                    cmds = res.decode(self.coding).split()
                    if hasattr(self,cmds[0]):
                        func = getattr(self,cmds[0])
                        func(cmds)
                except Exception:
                    break
            self.conn_close(self.conn)

    def get(self,cmds):
        """ 下载文件
        1.找到下载的文件
        2.发送 header_size
        3.发送 header_bytes file_size
        4.读文件 rb 发送 send(line)
        5.若文件不存在,发送0 client提示:文件不存在
        :param cmds: 下载的文件 eg:['get','3.jpeg']
        :return:
        """
        filename = cmds[1]
        file_path = os.path.join(self.down_filepath, filename)
        if os.path.isfile(file_path):
            header = {
                'filename': filename,
                'md5': 'xxxxxx',
                'file_size': os.path.getsize(file_path)
            }
            header_bytes = pickle.dumps(header)
            self.conn.send(struct.pack('i', len(header_bytes)))
            self.conn.send(header_bytes)
            with open(file_path, 'rb') as f:
                for line in f:
                    self.conn.send(line)
        else:
            self.conn.send(struct.pack('i', 0))

    def put(self,cmds):
        """ 上传功能
        1.接收4个bytes  得到文件的 header_size
        2.根据 header_size  得到 header_bytes  header_dic
        3.根据 header_dic  得到 file_size
        3.以写的形式 打开文件 f.write()
        :param cmds: 下载的文件 eg:['put','Amanda.jpg']
        :return:
        """
        obj = self.conn.recv(4)
        header_size = struct.unpack('i', obj)[0]
        header_bytes = self.conn.recv(header_size)
        header_dic = pickle.loads(header_bytes)
        print(header_dic)
        file_size = header_dic['file_size']
        filename = header_dic['filename']

        with open('%s/%s' % (self.upload_filepath, filename), 'wb') as f:
            recv_size = 0
            while recv_size < file_size:
                res = self.conn.recv(self.max_recv_bytes)
                f.write(res)
                recv_size += len(res)
tcp_server = TCPServer(('127.0.0.1',8080))
tcp_server.run()
tcp_server.server_close()
##客户端
import socket
import struct
import pickle
import os
class FTPClient:
    address_family = socket.AF_INET
    socket_type = socket.SOCK_STREAM
    # 下载的文件存放路径
    down_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'download')
    # 上传的文件存放路径
    upload_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'share')
    coding = 'utf-8'
    max_recv_bytes = 8192

    def __init__(self, server_address, connect=True):
        self.server_address = server_address
        self.socket = socket.socket(self.address_family, self.socket_type)
        if connect:
            try:
                self.client_connect()
            except Exception:
                self.client_close()
    def client_connect(self):
        self.socket.connect(self.server_address)
    def client_close(self):
        self.socket.close()

    def run(self):
        while True:
            # get 3.jpeg 下载   put Amanda.jpg 上传
            msg = input(">>>:").strip()
            if not msg: continue
            self.socket.send(msg.encode(self.coding))
            cmds = msg.split()
            if hasattr(self,cmds[0]):
                func = getattr(self,cmds[0])
                func(cmds)
    def get(self, cmds):
        """ 下载功能
        1.得到 header_size
        2.得到 header_types header_dic
        3.得到 file_size file_name
        4.以写的形式打开文件
        :param cmds: 下载的内容 eg: cmds = ['get','3.jpeg']
        :return:
        """
        obj = self.socket.recv(4)
        header_size = struct.unpack('i', obj)[0]
        if header_size == 0:
            print('文件不存在')
        else:
            header_types = self.socket.recv(header_size)
            header_dic = pickle.loads(header_types)
            print(header_dic)
            file_size = header_dic['file_size']
            filename = header_dic['filename']

            with open('%s/%s' % (self.down_filepath, filename), 'wb') as f:
                recv_size = 0
                while recv_size < file_size:
                    res = self.socket.recv(self.max_recv_bytes)
                    f.write(res)
                    recv_size += len(res)
                    print('总大小:%s 已下载:%s' % (file_size, recv_size))
                else:
                    print('下载成功!')

    def put(self, cmds):
        """ 上传功能
        1.查看上传的文件是否存在
        2.上传文件 header_size
        3.上传文件 header_bytes
        4.以读的形式 打开文件 send(line)
        :param cmds: 上传的内容 eg: cmds = ['put','a.txt']
        :return:
        """
        filename = cmds[1]
        file_path = os.path.join(self.upload_filepath, filename)
        if os.path.isfile(file_path):
            file_size = os.path.getsize(file_path)
            header = {
                'filename': os.path.basename(filename),
                'md5': 'xxxxxx',
                'file_size': file_size
            }
            header_bytes = pickle.dumps(header)
            self.socket.send(struct.pack('i', len(header_bytes)))
            self.socket.send(header_bytes)

            with open(file_path, 'rb') as f:
                send_bytes = b''
                for line in f:
                    self.socket.send(line)
                    send_bytes += line
                    print('总大小:%s 已上传:%s' % (file_size, len(send_bytes)))
                else:
                    print('上传成功!')
        else:
            print('文件不存在')
ftp_client = FTPClient(('127.0.0.1',8080))
ftp_client.run()
ftp_client.client_close()

5、基于 UDP协议的套接字

基于tcp协议通信的好处是传输数据可靠,因为它发出去 以后确认者必须回个信息,发送者才会把自己缓存的数据给清理掉,如果没有得到回复信息,它会从缓存中取出来,再发一次,再发;

而udp传输数据不可靠,因为它不管你收不收的到只管往外发;优点是不用建3次握手的链接,发数据效率高

from socket import * #会把socket所有的模块都拿到内存中
server=socket(AF_INET,SOCK_DGRAM) #又叫数据报协议,跟流式是有区别的
server.bind(('127.0.0.1',8080))
# #utp协议没有链接;只需要一个通信循环就可以了
while True:
    data,client_addr=server.recvfrom(1024)
    print(data)
    server.sendto(data.upper(),client_addr)
server.close()
#客户端

from socket import *
#没有链接不用发起链接
client = socket(AF_INET, SOCK_DGRAM)
while True:
    msg=input('>>: ').strip()
    #原来是基于链接的发,现在要明确的来发
    client.sendto(msg.encode('utf-8'),('127.0.0.1',8080))
    data,server_addr=client.recvfrom(1024)
    print(data,server_addr)
client.close()

验证下是否有粘包

##服务端
from socket import *
server=socket(AF_INET,SOCK_DGRAM)
server.bind(('127.0.0.1',8080))
res1=server.recvfrom(1024)
print('第一次:',res1)
res2=server.recvfrom(1024)
print('第二次:',res2)
server.close()

#打印出:

第一次: (b'hello', ('127.0.0.1', 50363))
第二次: (b'world', ('127.0.0.1', 50363))



# from socket import *
# server=socket(AF_INET,SOCK_DGRAM)
# server.bind(('127.0.0.1',8080))

# res1=server.recvfrom(1) #b'hello' #windows会报错,在linux上可以,只会接收一个h,其他数据会丢失
# print('第一次:',res1)
#
# res2=server.recvfrom(1024) #b'world'
# print('第二次:',res2)
# server.close()
#客户端
from socket import *
client = socket(AF_INET, SOCK_DGRAM)
client.sendto(b'hello',('127.0.0.1',8080)) #单独的数据就是一个包
client.sendto(b'world',('127.0.0.1',8080))
client.close()

用户加密认证登录:客户端连服务端直接就可以敲命令了,先输入密码,账号密码提交到服务端,服务端一定是存好了账号和密码;

配置文件的格式config.ini

原文地址:https://www.cnblogs.com/shengyang17/p/8766507.html