从网络谈到 Socket 编程

462 阅读19分钟

什么是网络

大家天天网上冲浪,对网络这个词肯定不会陌生,我们现在的生活中离不开网络,我们用微信等聊天软件,如果没有网络那只能自己跟自己聊天了......

如何能大家一起愉快的聊天呢?那就是联网了,即internet

然而internet为何物?

举一个简单的例子:如果把一个人和这个人的手机比作一台计算机,那这个人怎么与其他人进行沟通呢?两个人之间想要打电话首先一点必须是接电话线,这就好比是计算机之间的通信首先要有物理链接介质,比如网线,交换机,路由器等网络设备。

通信的线路建好之后,只是物理层面有了可以承载数据的介质,要想通信,还需要我们按照某种规则组织我们的数据,这样对方在接收到数据后就可以按照相同的规则去解析出数据,这里说的规则指的就是:中国有很多地区,不同的地区有不同的方言,为了全中国人都可以听懂,大家统一讲普通话

普通话属于中国国内人与人之间通信的标准,那如果是两个国家的人交流呢?

我们是不是也应该让两个国家的人说一种语言,于是有了世界统一的通信标准:英语

英语成为世界上所有人通信的统一标准,计算机之间的通信也应该有一个像英语一样的通信标准,这个标准称之为互联网协议, 可以很明确地说:互联网协议就是计算机界的英语,网络就是物理链接介质+互联网协议。

互联网协议

协议这个名词不仅局限于互联网范畴,也体现在日常生活中,比如情侣双方约定好在哪个地点吃饭,这个约定也是一种协议,比如你应聘成功了,企业会和你签订劳动合同,这种双方的雇佣关系也是一种 协议。注意自己一个人对自己的约定不能成为协议,协议的前提条件必须是多人约定。

那么网络协议是什么呢?

网络协议就是网络中(包括互联网)传递、管理信息的一些规范。如同人与人之间相互交流是需要遵循一定的规矩一样,计算机之间的相互通信需要共同遵守一定的规则,这些规则就称为网络协议。

没有网络协议的互联网是混乱的,就和人类社会一样,人不能想怎么样就怎么样,你的行为约束是受到法律的约束的;那么互联网中的端系统也不能自己想发什么发什么,也是需要受到通信协议约束的。

按照功能不同,人们将互联网协议分为OSI七层或TCP/IP五层或TCP/IP四层(我们只需要掌握tcp/ip五层协议即可),这种分层就好比是学习的几个阶段,每个阶段应该掌握专门的技能或者说完成特定的任

在这里插入图片描述

什么是TCP/IP?

TCP/IP 协议是我们程序员接触最多的协议,实际上,TCP/IP 又被称为 TCP/IP 协议簇,它并不特指单纯的 TCP 和 IP 协议,而是容纳了许许多多的网络协议。

然而,TCP/IP协议并不是国际官方组织制定的标准,而是民间组织(一些大型国际厂商、高等院校)自行商定的标准,因为更简便,推广力度更大,而成为了事实上的标准。

TCP/IP五层模型讲解

为了给网络协议的设计提供一个结构,网络设计者以分层的方式组织协议,每个协议属于层次模型之一,每一层都是向上提供服务。

OSI 模型共有七层,从下到上分别是物理层、数据链路层、网络层、运输层、会话层、表示层和应用层。但是这显然是有些复杂的。

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

物理层

一台计算机与另一台计算机要进行通信,第一件要做的事是什么?当然是要把这台计算机与另外的其他计算机连起来啊,这样,我们才能把数据传输过去。例如可以通过光纤啊,电缆啊,双绞线啊等介质把他们连接起来,然后才能进行通信。

在这里插入图片描述 物理层的协议使用链路层协议,这些协议与实际的物理传输介质有关

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

数据链路层

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

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

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

ethernet规定

  • 一组电信号构成一个数据包,叫做‘帧’
  • 每一数据帧分成:报头head和数据data两部分
    • head包含:(固定18个字节)
      • 发送者/源地址,6个字节
      • 接收者/目标地址,6个字节
      • 数据类型,6个字节
    • data包含:(最短46字节,最长1500字节)

head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址

mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址

在这里插入图片描述

PC1想要给PC4发送数据,是以广播的方式发送以太网包给PC4,然而PC3,PC2都会收到,拆开数据包后发现目标mac地址如果不是自己就丢弃,如果是自己就响应

广播:有了mac地址,同一网络内的两台主机就可以通信了

网络层

网络层由来:有了ethernet、mac地址、广播的发送方式,世界上的计算机就可以彼此通信了,问题是世界范围的互联网是由

一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网的广播方式,那么一台机器发送的包全世界都会收到,这就不仅仅是效率低的问题了,这会是一种灾难

必须找出一种方法来区分哪些计算机属于同一广播域,哪些不是,如果是就采用广播的方式发送,如果不是,就采用路由的方式

网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址

IP协议:

规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示,范围0.0.0.0-255.255.255.255

IP地址分类:IP地址根据网络ID的不同分为5种类型,A类地址、B类地址、C类地址、D类地址和E类地址。

子网掩码

所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。

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

我要区分网络位和主机位做什么?

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

IP地址分类

IP地址根据网络ID的不同分为5种类型,A类地址、B类地址、C类地址、D类地址和E类地址。

A类IP地址:一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”, 地址范围从1.0.0.0 到126.0.0.0。可用的A类网络有126个,每个网络能容纳1亿多个主机。

B类IP地址 :一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”,地址范围从128.0.0.0到191.255.255.255。可用的B类网络有16382个,每个网络能容纳6万多个主机 。

C类IP地址:一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”。范围从192.0.0.0到223.255.255.255。C类网络可达209万余个,每个网络能容纳254个主机。

D类地址用于多点广播(Multicast): D类IP地址第一个字节以“lll0”开始,它是一个专门保留的地址。 它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中。多点广播地址用来一次寻址一组计算机,它标识共享同一协议的一组计算机。

E类IP地址 以“llll0”开始,为将来使用保留。

传输层

传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,迅雷等多个应用程序。

那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序呢?答案就是端口,端口即应用程序与网卡关联的编号。

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

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

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

  • TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,当应用程序采用 TCP 发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。TCP 为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提高网络利用率等众多功能。
  • UDP 是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在 UDP 的情况下,虽然可以确保发送消息的大小,却不能保证消息一定会到达。因此,应用有时会根据自己的需要进行重发处理。
  • TCP 和 UDP 的优缺点无法简单地、绝对地去做比较:TCP 用于在传输层有必要实现可靠传输的情况;而在一方面,UDP 主要用于那些对高速传输和实时性有较高要求的通信或广播通信。TCP 和 UDP 应该根据应用的目的按需使用

TCP/IP五层模型讲解:参考文章

Socket介绍

通过前面的介绍我们已经知道,假设我现在要写一个程序,给另一台计算机发数据,必须通过tcp/ip协议 ,但具体的实现过程是什么呢?我应该怎么操作才能把数据封装成tcp/ip的包,又执行什么指令才能把数据发到对端机器上呢?

此时,socket隆重登场,socket是一种抽象层,应用程序通过它来发送和接受数据,使用Socket可以将应用程序添加到网络中,与处于同一网络中的其他应用程序进行通信。

简单来说,Socket提供了程序内部与外界通信的端口并为通信双方提供数据传输通道

什么是socket?

socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。白话说,socket就是两个节点为了互相通信,而在各自家里装的一部'电话'。

我们知道两台计算机之间数据传输,要通过网卡。网卡归谁管?操作系统。 所以说我写的程序,只要把数据给操作系统,操作系统调用网卡去发就好了。

我的程序怎么把数据给操作系统呢?

回想一下,我程序要打开存于硬盘上的文件,是不是调用file就可以了,file是不是就是我程序和操作系统之间的一个接口呢?

同样的道理,socket也是这么一个接口,用于程序和操作系统之间,进行网络数据收发的接口。在面向过程的语言中,socket是一个函数,在面向对象的语言中,socket是一个class,无论哪样,都是程序和操作系统之间的一个接口。

在调用socket时,我们是需要指定协议的,如果指定tcp,那么这个socket就用tcp跟对方通信,如果指定udp,那么socket就用udp跟对方通信

UDP发送与接收程序

在这里插入图片描述

UDP发送数据

import socket

def main():
    # 创建一个udp套接字
    udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

    # 可以使用socket收发数据
    # 发送的内容(是字节不是字符串)
    udp_socket.sendto(b'nihao',('192.168.0.162',8080))
    # 关闭socket
    udp_socket.close()

if __name__ == '__main__':
    main()

网络调试助手接收数据 在这里插入图片描述

发送任意的数据给网络调试助手,优化程序

import socket

def main():
    udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    
    while True:
        send_data = input("请输入要发送的数据:")
        if send_data == 'q':
            break
        # 字符串转字节
        send_data = send_data.encode('utf-8')
    
        udp_socket.sendto(send_data,('192.168.0.162',8080))
    
    # 关闭连接
    udp_socket.close()

if __name__ == '__main__':
    main()

UDP接收数据

  • 1 创建套接字
  • 2 绑定本地信息(IP和端口)
  • 3 接受数据
  • 4 打印数据
  • 5 关闭套接字
import socket

def main():
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    # 绑定本地的相关信息,如果一个网络程序不绑定,则系统会随机分配
    local_addr = ('', 7789) # ip地址和端口号,ip一般不用写,表示本机的任何一个ip
    udp_socket.bind(local_addr)
    
    while True:
        # recvfrom 表示接收数据
        recv_data = udp_socket.recvfrom(1024) # 1024表示本次接收的最大字节数
        # 是一个元组(接收到的数据,(发送方的ip,port))
        print(recv_data)
        recv = recv_data[0]
        send_addr = recv_data[1]
        # 如果用utf-8程序报错,Windows里面默认编码是gbk
        # print("%s:%s"%(str(send_addr),recv.decode('utf-8')))
        print("%s:%s"%(str(send_addr),recv.decode('gbk')))
    
    udp_socket.close()
    
if __name__ == '__main__':
    main()

UDP聊天器

import socket

def send_data(udp_socket):
    """ 先完成发送数据,测试 """
    send_data = input("请输入要发送的数据:")
    dest_ip = input("请输入要发送的IP:")
    dest_port = int(input("请输入要发送的端口:"))
    # 发送数据
    udp_socket.sendto(send_data.encode('utf-8'), (dest_ip, dest_port))

def recv_data(udp_socket):
    """ 接收数据 """
    recv_data = udp_socket.recvfrom(1024) # 程序到这里会阻塞
    print("%s:%s" % (recv_data[1], recv_data[0].decode()))


def main():
    # 创建套接字 套接字是可以同时收发数据的
    udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

    udp_socket.bind(("",7788))
    while True:
        send_data(udp_socket)
        recv_data(udp_socket)

if __name__ == '__main__':
    main()

TCP发送接收数据

在这里插入图片描述

TCP分为客户端和服务端

  • 客户端指运行在用户设备上的程序
  • 服务端指运行在服务器设备上的程序,专门为客户端提供数据服务

TCP客户端构建流程

  • 1.创建socket
  • 2.链接服务器
  • 3.接收/发送数据(最大接收1024个字节)
  • 4.关闭套接字
import socket

def main():
    tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    # server_ip = input("server ip:")
    # server_port = int(input("server port:"))
    # server_addr = (server_ip,server_port)
    # tcp_client.connect(server_addr)
    tcp_client.connect(("192.168.0.162",8080))

    send_data = input("发送的数据:")
    
    # 这里用send,因为已经连接过服务器
    tcp_client.send(send_data.encode())
    
    recvData = tcp_client.recv(1024)
    print('接收到的数据为:', recvData.decode('gbk'))    # 这里也可以改成utf-8

    tcp_client.close()

if __name__ == '__main__':
    main()

TCP服务端

如果想让别人能更够打通咱们的电话获取相应服务的话,需要做以下几件事情:

  • 1 买个手机
  • 2 插上手机卡
  • 3 设计手机为正常接听状态
  • 4 等待别人拨打电话

对应socket流程

  • 1 socket创建套接字
  • 2 bind绑定IP和port (不能让客户端,每次都去问)
  • 3 listen使套接字变为可以被动链接
  • 4 accept等待客户端的链接
  • 5 recv/send接收发送数据
import socket

def main():
    # 创建套接字
    tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # 绑定本地信息
    tcp_server.bind(("",7890))
    # 让默认的套接字由主动变为被动,这样就可以接收别人的链接了  创建出来套接字默认是链接别人,而不是别人链接你
    tcp_server.listen(128)

    # 等待客户端的链接 程序会阻塞
    # new_client_socket 用来为这个客户端服务
    # tcp_server就可以省下来服务其他新客户端的链接
    new_client_socket,client_addr = tcp_server.accept()
    # 客户端的IP和端口
    print(client_addr)

    # 接受客户端发送过来的数据
    # 对于recvfrom 可同时应用于面向连接的和无连接的套接字。
    # recv一般只用在面向连接的套接字,几乎等同于recvfrom,
    recv_data = new_client_socket.recv(1024)
    print(recv_data)

    # 回数据给客户端
    new_client_socket.send("haha".encode('utf-8'))

    # 关闭客户端
    new_client_socket.close()
    tcp_server.close()


if __name__ == '__main__':
    main()

循环为多个客户端服务

import socket

def main():
    # 创建套接字
    tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # 绑定本地信息
    tcp_server.bind(("",7891))
    
    # 1.为多个客户端服务
    while True:
        print("等待新的客户端来连接.....")
        # 让默认的套接字由主动变为被动
        tcp_server.listen(128)

        # 等待客户端的链接
        new_client_socket,client_addr = tcp_server.accept()
        # 客户端的IP和端口
        print(client_addr)
        
        # 2.循环为一个客户端服务多次
        while True:
            # 接受客户端发送过来的数据
            recv_data = new_client_socket.recv(1024)
            print(recv_data.decode('gbk'))
            
            # 如果recv解堵塞,2种方式
            # 1.客户端发送过来数据
            # 2.客户端调用close,导致recv解堵塞
            if recv_data:
                # 回数据给客户端
                new_client_socket.send("haha".encode('utf-8'))
            else:
                break
        # 关闭客户端 意味着不会为这个客户端服务
        new_client_socket.close()
        print("已经服务完毕")
        
    # 如果将监听套接字关闭了,那么会导致 不能再次等待新客户端的到来
    tcp_server.close()

if __name__ == '__main__':
    main()

socketserver

我们上面的案例虽然可以为多个客户端服务,但是不能同时为多个客户端服务,那有没有什么本法可以解决呢?

是可以通过socketserver模块来实现并发,同时为多个客户端服务器

import socketserver


class MyServer(socketserver.BaseRequestHandler):

    def handle(self):

        print(self.request)
        conn = self.request

        while True:
            conn.send(b'hello')
            print(conn.recv(1024))


server = socketserver.ThreadingTCPServer(('192.168.100.158', 9003), MyServer)
server.serve_forever()

这是一个简易版的socketserver并发,我们可以稍微完善一下代码

import socketserver

# 自定义一个处理通信的类,继承BaseRequestHandler
class MyRequestHandle(socketserver.BaseRequestHandler):
    
    # 重写父类的handle方法,该方法实现通信循环
    def handle(self):
        # 如果tcp协议,self.request => conn
        # 如果tcp协议,self.client_address => client_addr
        while True:
            try:
                msg = self.request.recv(1024)
                if len(msg) == 0: break
                self.request.send(msg.upper())
            except Exception:
                break
        self.request.close()


s = socketserver.ThreadingTCPServer(('127.0.0.1',8889),MyRequestHandle)
# server_forever() 不停的循环等到那客户端请求连接
s.serve_forever()

开发自己的静态Web服务器

实现步骤:

  • 编写一个TCP服务端程序
  • 获取浏览器发送的http请求报文数据
  • 读取固定页面数据,把页面数据组装成HTTP响应报文数据发送给浏览器。
  • HTTP响应报文数据发送完成以后,关闭服务于客户端的套接字。
import socket


if __name__ == '__main__':
    # 创建tcp服务端套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置端口号复用, 程序退出端口立即释放
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    # 绑定端口号
    tcp_server_socket.bind(("192.168.100.79", 9000))
    # 设置监听
    tcp_server_socket.listen(128)
    while True:
        # 等待接受客户端的连接请求
        new_socket, ip_port = tcp_server_socket.accept()
        # 代码执行到此,说明连接建立成功
        recv_client_data = new_socket.recv(4096)
        # 对二进制数据进行解码
        recv_client_content = recv_client_data.decode("utf-8")
        print(recv_client_content)

        with open("static/index.html", "r") as file:
            # 读取文件数据
            file_data = file.read()

        
        # 把数据封装成http 响应报文格式的数据
        
        # 响应报文

        # 响应行
        response_line = "HTTP/1.1 200 OK\r\n"
        # 响应头
        response_header = "Server: PWS1.0\r\n"

        # 空行

        # 响应体
        response_body = file_data

        # 拼接响应报文
        response_data = response_line + response_header + "\r\n" + response_body

        # 发送数据
        new_socket.send(response_data.encode('utf-8'))

        # 关闭服务与客户端的套接字
        new_socket.close()

返回指定页面数据

目前的Web服务器,不管用户访问什么页面,返回的都是固定页面的数据,接下来需要根据用户的请求返回指定页面的数据

返回指定页面数据的实现步骤:

  • 获取用户请求资源的路径
  • 根据请求资源的路径,读取指定文件的数据
  • 组装指定文件数据的响应报文,发送给浏览器
  • 判断请求的文件在服务端不存在,组装404状态的响应报文,发送给浏览器
import socket


def main():
    # 创建tcp服务端套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置端口号复用, 程序退出端口立即释放
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    # 绑定端口号
    tcp_server_socket.bind(("192.168.100.79", 9000))
    # 设置监听
    tcp_server_socket.listen(128)
    while True:
        # 等待接受客户端的连接请求
        new_socket, ip_port = tcp_server_socket.accept()
        # 代码执行到此,说明连接建立成功
        recv_client_data = new_socket.recv(4096)
        # 用网络调试助手,连接上,关闭 此时收不到数据,下面的切割就会报错
        if len(recv_client_data) == 0:
            print("关闭浏览器了")
            new_socket.close()
            # return
            continue

        # 对二进制数据进行解码
        recv_client_content = recv_client_data.decode("utf-8")
        # print(recv_client_content)
        # 根据指定字符串进行分割, 最大分割次数指定2 , maxsplit=2
        request_list = recv_client_content.split(" ")
        print(request_list)
        # 获取请求资源路径
        request_path = request_list[1]

        # 判断请求的是否是根目录,如果条件成立,指定首页数据返回
        if request_path == "/":
            request_path = "/index.html"

        try:
            # 动态打开指定文件, 使用rb模式, 兼容图片文件
            with open("static" + request_path, "rb") as file:
                # 读取文件数据
                file_data = file.read()
        except Exception as e:
            # 请求资源不存在,返回404数据
            # 响应行
            response_line = "HTTP/1.1 404 Not Found\r\n"
            # 响应头
            response_header = "Server: PWS1.0\r\n"

            with open("static/error.html", "rb") as file:
                file_data = file.read()
            # 响应体
            response_body = file_data

            # 拼接响应报文 \r\n 是空行
            response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
            # 发送数据
            new_socket.send(response_data)
        else:
            # 没有异常
            # 响应行
            response_line = "HTTP/1.1 200 OK\r\n"
            # 响应头
            response_header = "Server: PWS1.0\r\n"

            # 响应体
            response_body = file_data

            # 拼接响应报文
            response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
            # 发送数据
            new_socket.send(response_data)
        finally:
            # 无论是否有异常,都会执行的代码
            # 关闭服务与客户端的套接字
            new_socket.close()

if __name__ == '__main__':
    main()