第33天 初识socket编程

发布时间:2019-03-31 20:59:24编辑:auto阅读(1755)

    目录

      1. OSI七层模型简介

      2. 网络编程简介

      3. socket编程简介

      4. 使用socket进行网络编程

      5. 基于tcp的socket编程

      6. TCP粘包问题

      7. 基于udp的socket编程

    一. OSI七层模型简介

      从下图中我们可以看到OSI七层模型中规定的几个特殊的名词:

      MAC:  mac地址是在每一个计算机出厂的时候就会烧录进网卡内的一串数字,用来唯一的表示一台计算机

      IP:  ip地址根据你所在的网络的不同而不同,主要是用来标识一个局域网。

      PORT:端口号的范围是0-65535,主要是用来标识唯一的一个应用软件。

      通过以上mac和ip我们可以在全世界找到唯一的一台计算机,然后再通过port我们可以找到计算机内的唯一的一个应用程序,从而完成两台计算机应用程序之间的交互。

    二. 网络编程简介

      什么叫做网络编程呢?在之前我们写程序的时候,所有的程序都是在一台机器之上的,所以我们不需要考虑通过internet去交互数据。但是在日常的开发过程中,我们写的程序大都不是在一台机器之上的,这就会出现一个问题,如果程序不在一台机器之上,我们应该通过什么进行通信呢?那就是网络,而我们把这种编程的方式称之为网络编程。下面是网络编程的两种架构

    c/s架构
      c:客户端
      s: 服务器
      典型的应用:qq,微信,王者荣耀
    b
    /s架构
      b: 浏览器
      s: 服务器
      典型的应用: Google,火狐等

    三. socket编程简介

      socket是什么呢?socket编程其实就是网络编程。如下图,socket并不是一个真实存在的协议或者标准,它只是应用层与tcp/ip软件协议簇通信的中间软件抽象层。更准确的来说它就是别人规定好的一个模块,这个模块给我们封装了一系列的接口,可以让我们很方便的进行网络编程,而不用去深究我们应该怎么去写一个三次握手,四次挥手或者报文封装之类的复杂操作。

      虽然socket编程给我们封装了一系列的接口,但是并不代表我们就不需要去学习底层的协议。

    四. 使用socket进行网络编程

      socket编程一般是有两个程序的,一个是服务端,一个是客户端,一个简单的socket编程用到方法如下图。

      

    案例: 接下来我们以打电话为例来简单的介绍一下socket程序

    步骤0:初识代码

    import socket  # 导入了模块
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 首先我去买个手机
    phone.bind(('127.0.0.1', 8080)) # 给我的手机插一张卡,('127.0.0.1', 8080)
    phone.listen(5) # 开始给我的手机装电池进行开机
    conn, addr = phone.accept() # 开机之后我随时等待着有人打电话过来
    data = conn.recv(1024)  # 先听一下对面说什么
    conn.send('hello'.encode('utf-8'))  # 然后给对面回复一个内容
    
    conn.close()  # 话讲完了之后我要按一下挂断才能挂断电话
    phone.close()  # 今天有点累了,把手机调成飞行模式,不想接电话了

     步骤一: 创建一个socket对象

    
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    参数一:定义的是套接字家族
      套接字家族其实分为两种,在最初unix开发出来的时候套接字其实为了解决进程间通信的,也就是通常所说的基于文件的套接字家族。随着计算机的发展,网络间的通信越来越频繁,因此慢慢的形成了另外一个家族,那就是基于网络的套接字家族,我们现在所学习的套接字基本山都是基于网络的套接字家族
      socket.AF_INET: 基于网络的套接字家族
      socket.AF_UNIX: 基于文件的套接字家族
    参数二:定义的是传输层使用的协议
      socket.SOCK_STREAM:使用tcp协议
      socket.SOCK_DGRAM: 使用的是udp协议

    步骤二: 绑定ip和端口

    phone.bind(('127.0.0.1', 8080))
    解释:bind操作只有在服务端才会出现,为了通知客户端此时服务端在网络的哪个位置,以及是哪个应用。
    参数:套接字地址(也就是ip+端口)

    ip: ip就是运行此服务的计算机的ip地址,此处的127.0.0.1是一个特殊的回环地址,用来表示本机,一般用来测试代码用的。
    port: 端口用来表示是哪个应用程序。 1-1023用来给系统使用,1024-65535我们可以正常的使用,但是一般要避免使用约定俗成的一些端口,如数据库的端口3306。

    步骤三:监听

    phone.listen(5)  # 用来监听的
    参数:定义的是半连接池的大小

    5: 代表的就是半连接池只有5个,如果超过5个,将直接拒绝连接。注意,此处的半连接数量指的并不是已经建立了tcp连接的数量。

     半连接池数的例子:

    # 服务端代码
    
    import  socket
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 65535))
    server.listen(5)  # 此处定义半连接数为5,也就是当前没有连接成功的数量必须小于等于5
    while True:
        conn, addr = server.accept()
        while True:
            try:
                res = conn.recv(1024)
                print('服务端发送的数据', res)
            except Exception as e:
                print(e)
                break
        conn.close()
    
    
    # 客户端
    import  socket
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    client.connect(('127.0.0.1', 65535))
    while True:
        msg = input('>>>:')
        client.send(msg.encode('utf-8'))端代码
    测试代码

    当我连接了第七个客户端的时候就会报错,为什么呢?

    1. 第一个客户端发送syn请求的时候,首先进入了半连接池内,此时服务端空闲,因此是可以进行三次握手的,因此服务端将此连接从半连接池中拿出来,然后进行三次握手,然后进行通信。

    2. 第二个客户端紧接着也发送了syn请求,它也进入了半连接池内,但是此时的服务端在忙着和客户端一进行通信,因此第二个客户端就等待着服务端结束了与第一个通信之后过来和自己通信,第三个客户端第四个。。都是一样的。

    3. 当第七个客户端发送了syn请求之后,服务端发现现在我的半连接池里面已经有了五个客户端在等待了,而listen的参数也是五,不能再接受请求了,因此立即给你回复一个报文告诉你不能连接,也就是下图中的错误了。

    步骤四接收一个连接

    conn, addr = phone.accept()
    解释:当代码执行到这一步的时候,程序就会阻塞,就像input函数在等待输入一样,它在等待着一个连接,当一个连接到来的时候,会返回一个元组,元组的内容为双向连接的对象,和一个地址。

    conn: 接收一个连接之后就相当于建立了一根管道,此管道是服务端和客户端共同维持的,我们可以通过conn去发送和接受数据
    addr: (ip, port),这个地址指的是当前连接的客户端的地址和端口

    步骤五: 接收数据

    data = conn.recv(1024) 
    解释:代表从管道conn中接收一个最大为1024的数据。

    参数:此处的1024代表的是最大一次能从系统缓存中得到的字节数。
    举例一:系统缓冲区中有1025个字节,如果用conn.recv(1024)去取,就只能取到1024个字节,剩余的一个字节还留在系统缓冲区
    举例二:系统缓冲区有25个字节,如果用conn.recv(1024)去取,能够取完25个字节,此时系统缓冲区没有字节。

    步骤六: 发送数据

    conn.send('hello'.encode('utf-8'))
    解释:就是通过管道conn将数据发送给客户端

    注意:在socket编程中发送和接收的数据都是字节,因此在发送和接收的时候我们需要对其进行编码和解码。

    五. 基于tcp的socket编程

    1. 简单实现套接字通信

    import socket
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 65535))
    server.listen(5)
    conn, addr = server.accept()
    data = conn.recv(1024)
    conn.send(data.upper() + b'sb')
    服务端代码
    import socket
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 65535))
    msg = input('>>')
    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print('服务器回复的数据', data)
    客户端代码

    以上代码可以完成一个简单的通信过程,当客户端输入一串字符之后,服务端都会在后面加上sb两个字符返回回来。

    2. 循环通信

    虽然实现了一个简单的通信,但是在日常生活中我们说一句话之后,就会挂电话吗,当然不会,我们希望的是通过一个循环可以让我模拟出我们可以不停的发送信息然后服务端给我回复信息,因此代码可以修改成下面的样子。

    import socket
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 65535))
    while True:
        msg = input('>>')
        client.send(msg.encode('utf-8'))
        data = client.recv(1024)
        print('服务器回复的数据', data)
    客户端程序
    import socket
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 65535))
    server.listen(5)
    conn, addr = server.accept()
    while True:
        data = conn.recv(1024)
        conn.send(data.upper() + b'sb')
    服务端程序

    问题一: 当我们输入为空的时候,客户端和服务端都会进入等待状态

      机制: Python默认是不会发送空数据到服务端的。

      客户端:所以当客户端输入为空,执行send时发现数据为空,就直接跳过此语句,此时客户端开始执行recv语句,也就是进入了阻塞阶段,等待接收数据,但是此时并没有人给我发数据,所以阻塞了。

      服务端:因为一直没有收到客户端的数据,因此也一直阻塞状态中

      解决方法:我们可以在send之前判断输入是否为空,如果为空,则continue

    问题二:当把客户端异常退出的时候,服务端也异常退出

      服务端与客户端在连接成功的那一刻会返回一个conn对象,这个对象是客户端和服务端同时维护的,因此无论是哪一方异常断开都会出现异常。

      解决方法: 在服务端进行捕捉异常,如果捕捉到异常,则关闭当前连接,继续接受其他的连接。

    import socket
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 65535))
    server.listen(5)
    conn, addr = server.accept()
    while True:
        # 在此处捕捉异常,一旦捕捉到异常则退出循环关闭当前连接
        try:
            data = conn.recv(1024)
            conn.send(data.upper() + b'sb')
        except Exception as e:
            print(e)
            break
    conn.close()
    server.close()
    修改后的服务端代码
    import socket
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 65535))
    while True:
        msg = input('>>')
        if not msg:
            continue
        client.send(msg.encode('utf-8'))
        data = client.recv(1024)
        print('服务器回复的数据', data)
    修改后的客户端代码

    3. 循环连接

      在我们上面修改代码的基础上,当客户端关闭之后,服务端虽然不会进行报错了,但是还是会退出来,为了让他循环连接,所以再加一个循环,实现客户端退出之后,将不会影响服务端下次的连接

    服务端:

    import socket
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 65535))
    server.listen(5)
    while True:   conn, addr
    = server.accept()   while True:    # 在此处捕捉异常,一旦捕捉到异常则退出循环关闭当前连接   try:    data = conn.recv(1024)    conn.send(data.upper() + b'sb')   except Exception as e:    print(e)    break   conn.close() server.close()

    客户端:

    import socket
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 65535))
    while True:
        msg = input('>>')
        if not msg:
            continue
        client.send(msg.encode('utf-8'))
        data = client.recv(1024)
        print('服务器回复的数据', data)

    六.  TCP粘包问题

    1. 什么是粘包

      两个应用程序在交互数据的时候并不是直接进行交互的,如下图所示。

      发送过程:应用程序要把数据首先存放在操作系统的缓冲区,然后由操作系统去调用网卡接口将数据转发出去。

      接收过程:网卡接收到数据之后首先要存放在操作系统的缓冲区,然后才是由应用程序过来取。

      粘包:指的是连续发送两个不相干的数据包,但是应用程序却没有来得及收,从而导致两个不相干的数据包一起存在操作系统缓冲区而无法区分的问题。

    2. 为什么会出现粘包这个问题

      因为对于操作系统而言,每接收一个数据包就会在之前数据存储的后面继续存储数据,并不会给两个数据包区分一个明显的界限,因此当上一次的数据没有取完的时候,一旦操作系统重新接收到了新的数据,就会出现粘包的问题。

    3. 粘包的现象

    import socket
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 65535))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        while True:
            try:
                data = conn.recv(1024)
                conn.send(data.upper() + b'sb')
            except Exception as e:
                print(e)
                break
        conn.close()
    server.close()
    上面写好的服务端代码
    import socket
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 65535))
    while True:
        msg = input('>>')
        if not msg:
            continue
        client.send(msg.encode('utf-8'))
        data = client.recv(5)   # 此时接收的字节数改变了
        print(data)
    上面写好的客户端代码,把接收的字节数改成了5,用来实验

    执行客户端测试原理:

    我们可以让客户端的代码睡一觉,就会出现另外一种结果:

    4. 处理粘包的三种方式

    <1>.time.sleep()发送完数据之后主动让代码睡一会

    time.sleep()只能在某一种情况下去解决粘包的问题,在数据量较大的时候也不能解决粘包的问题,而且我们也并不建议这样子去做。

    <2>.增大recv()的参数值

    这个方法确实是可以在一定程度上解决粘包的问题,但是应用程序缓冲区的大小并不是你想要多大就多大的,而且,参数值得也是有限制的,因此,我们也不建议这样子去做。

    <3>.在发送数据之前发送一下数据的长度 

    远程执行命令的小程序

    import socket
    import subprocess
    import struct
    
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 65535))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        while True:
            try:
                cmd = conn.recv(1024)
                print(cmd)
                obj = subprocess.Popen(
                    cmd.decode('utf-8'),
                    shell=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE
                )
                print(obj)
                # 注意此处的stdout和stderr不能位置不能相反,必须先读out后读err
                stdout = obj.stdout.read()
                print(stdout)
                stderr = obj.stderr.read()
                print(stderr)
                # 计算数据的长度,返回的是一个字节,在客户端先读取四个字节获得数据长度
                head = struct.pack('i', len(stderr + stdout))
                print(head)
                # 拼接数据,并进行发送
                data = head + stderr + stdout
                print(len(stdout) + len(stderr))
                conn.send(data)
            except Exception as e:
                print(e)
                break
        conn.close()
    服务端程序
    import socket
    import struct
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 65535))
    while True:
        msg = input('>>')
        if not msg:
            continue
        client.send(msg.encode('utf-8'))
        head = client.recv(4)    # 获得报头
        total_len = struct.unpack('i', head)[0]  # 将报头中前四个字节解压得到数据的总长度
        current_len = 0    # 当前读取的报文长度
        data = b''
        finally_data = b''
        while current_len < total_len:
            print(current_len, total_len)
            data = client.recv(1024)
            current_len += len(data)
            finally_data += data
        print(finally_data.decode('gbk'))
    客户端程序

    5. 自定义报头

    自定义报头和我们之前说的那个远程执行命令的小程序很相似,只是在客户端进行解析的时候需要通过两步去操作,一个就是分析报头,一个就是分析数据包真实的内容。
    """客户端传过来一个名字,复制这个文件到客户端上面,并且在自定义一个文件头传递过去"""
    
    import socket
    import hashlib
    import struct
    import json
    
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(('127.0.0.1', 8080))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        while True:
            try:
                file_name = conn.recv(1024)  # 获得一个字节形式的文件名
                # print(file_name)
                hash_value = hashlib.md5()   # 当前文件的hash值
                file_data_bytes = b''  # 存储文件的数据
                with open(file_name.decode('utf-8'), 'rb') as f:
                    for line in f:
                        file_data_bytes += line
                        hash_value.update(line)
    
                # 自定义文件头
                head_dict = {
                    'name': file_name.decode('utf-8'),
                    'md5': hash_value.hexdigest(),
                    'file_data_len': len(file_data_bytes)
                }
                print(head_dict)
                # 用json转换当前文件头为字节形式
                head_bytes = json.dumps(head_dict).encode('utf-8')
    
                # 计算当前文件头的长度,并且转换成固定长度(i为4)的字节形式
                head_len_bytes = struct.pack('i', len(head_bytes))
    
                # print(head_len_bytes,)
    
                # 发送数据
                conn.send(head_len_bytes + head_bytes + file_data_bytes)
    
            except Exception as e:
                print(e)
                break
        conn.close()
    服务端程序
    import json
    import socket
    import struct
    import os
    
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 8080))
    
    temp = ''
    while True:
        file_name = input(temp + '输入你要下载的文件名称(Q退出)>>>').strip()
        file_name = 'H:\python_study\day34\自定义\客户端.py'
        if file_name.upper() == 'Q':
            break
        if not file_name:
            print('输入不能为空')
            temp = '请重新'
            continue
        client.send(file_name.encode('utf-8'))
        # print(client.recv(100))
        # 读取文件头的内容
        head_len = struct.unpack('i', client.recv(4))[0]  # 获取文件头的长度
        print(head_len)
        head_data_bytes = b''
        while head_len > 0:
            if head_len - 10 > 0:
                head_data_bytes += client.recv(10)
            else:
                head_data_bytes += client.recv(head_len)
            head_len -= 10
        print(head_data_bytes.decode('utf-8'))
        head_data_dict = json.loads(head_data_bytes.decode('utf-8'))
        print(head_data_dict)
        # 读取真实的文件数据并且进行保存
        file_data_len = head_data_dict['file_data_len']
        file_data_bytes = b''
        while file_data_len > 0:
            file_data_bytes += client.recv(10)
            file_data_len -= 10
    
        file_path = os.path.normpath(os.path.join(
            __file__,
            os.pardir,
            '下载',
            os.path.basename(head_data_dict['name'])
        ))
        with open(file_path, 'wb') as f:
            f.write(file_data_bytes)
    client.close()
    客户端程序

    七. 基于udp的socket编程

    1. 简单的实现套接字的通信

    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server.bind(('127.0.0.1', 8080))   # 和tcp一样需要去绑定ip和端口,但是它不必像tcp一样去监听和接收连接
    conn = server.recvfrom(1024)  # 直接recvfrom就可以了
    print(conn)
    
    # 结果:
    # (b'hello', ('127.0.0.1', 55802))   会获得一个地址和数据
    import socket
    
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    client.sendto('hello'.encode('utf-8'), ('127.0.0.1', 8080))  # 客户端需要发送数据的时候需要添加一个ip和端口

    2. 实现循环通讯

    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server.bind(('127.0.0.1', 8080))   # 和tcp一样需要去绑定ip和端口,但是它不必像tcp一样去监听和接收连接
    while True:
        data, addr = server.recvfrom(1024)  # 直接recvfrom就可以了
        server.sendto(data.upper(), addr)
    服务端程序
    import socket
    
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    while True:
        msg = input('>>>')
        client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))  # 客户端需要发送数据的时候需要添加一个ip和端口
        data, addr = client.recvfrom(1024)
        print('服务器发送过来的数据>', data.decode('utf-8'))
    客户端程序

    3. 实现两个客户端之间的通信

    思路:每一个给服务器发送过数据的客户端都会在服务器上面保存一个地址,然后等下次客户端再给服务器发送数据的时候,服务器就会将客户端的消息发送给所有的客户端
    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server.bind(('127.0.0.1', 8080))   # 和tcp一样需要去绑定ip和端口,但是它不必像tcp一样去监听和接收连接
    
    client_set = set({})
    while True:
        data, addr = server.recvfrom(1024)  # 直接recvfrom就可以了
        client_set.add(addr)
        for client in client_set:
            server.sendto(data, client)
    服务器端代码
    import socket
    
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    while True:
        msg = input('>>>')
        client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))  # 客户端需要发送数据的时候需要添加一个ip和端口
        data, addr = client.recvfrom(1024)
        print('服务器发送过来的数据>', data.decode('utf-8'))
    客户端代码都是一样的

     4. udp的recvfrom(512)参数的意义

    import socket
    
    client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    while True:
        msg = input('>>')
        client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    服务端代码
    import socket
    
    client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    while True:
        msg = input('>>')
        client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    客户端代码
    windows:
      当我们在客户端输入12345678910也就是发送超过十个字节之后,就会报错。如下图
    linux:
      不会报错,但是会丢包,也就是只会收到10个字节,其他的字节都不收了。

    注意:一般我们只会把此处的值设置成512,因为对于udp而言,如果一旦接收的字节超过512之后,就会极易出现丢包的现象。

     

关键字