Python网络编程 01 网络基础、socket 抽象层、粘包

一、网络基础

1、OSI七层模型

互联网协议按照功能不同分为osi七层或tcp/ip五层或tcp/ip四层
在这里插入图片描述
(1)物理层功能就是用网线等传输介质去传输0101一堆的二进制信号。
(2)数据链路层对0101信号进行分组,遵循以太网协议。一组电信号叫做一个数据帧。数据帧=head +data。本层是广播通信,靠mac地址进行识别主机。
(3)网络层是靠IP地址及子网掩码计算出相应的子网,根据一系列的路由规则找到该子网。
(4)传输层找到该主机后,依靠端口号找到对应的程序。是端对端的传输。其中,0-1023为系统端口号,1024-65535是可以自己用的端口号。该层遵循协议为TCP和UDP。
(5)应用层功能:规定应用程序的数据格式

2、arp协议

arp协议为数据链路层协议。
发包步骤:
(1)计算是否在一个广播域
(2)arp协议:
①在一个广播域中:
在这里插入图片描述
②不在一个广播域中:
在这里插入图片描述

3、tcp三次握手和四次挥手

在这里插入图片描述

二、socket 抽象层

1、什么是socket

socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。
在这里插入图片描述

2、socket分类

(1)基于文件类型的套接字家族 AF_UNIX
(2)基于网络类型的套接字家族 AF_INET。在网络编程中,我们只使用AF_INET。

3、数据传输流程

在这里插入图片描述

4、基于TCP的套接字代码示例

服务端:

from socket import *
ip_port=("192.168.1.2",8000)
back_log = 5    #定义半链接池大小
buffer_size = 1024  #每次从缓冲区接收的最大字节数

tcp_server = socket(AF_INET,SOCK_STREAM)    #创建套接字对象,tcp协议要选SOCK_STREAM
tcp_server.bind(ip_port)    #把ip地址和端口号绑定到套接字
tcp_server.listen(back_log) #监听链接

while True:
    print("服务器开始运行:")
    conn,addr = tcp_server.accept() #与客户端的connect对应,等待触发三次握手,建立双向链接
    print("双向链接是:",conn)
    print("客户端地址是:",addr)

    while True:
        data = conn.recv(buffer_size)   #等待接受客户端send的数据
        if not data:
            break
        print("客户端发来的消息是:",data.decode("utf-8"))
        conn.send(data.upper()) #向客户端发送消息

    conn.close()    #关闭链接,即触发四次挥手

tcp_server.close()  #关闭socket对象

客户端:

from socket import *
ip_port=("192.168.1.2",8000)
back_log=5
buffer_size=1024

tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port) #尝试连接服务器

while True:
    try:
        msg = input(">>>").strip()
        if not msg:
            continue
        tcp_client.send(msg.encode("utf-8"))    #向服务端发送数据
        print("客户端已经发送消息:")
        data = tcp_client.recv(buffer_size) #接受服务端发来的数据
        print("收到服务端发来的消息:",data.decode("utf-8"))
    except KeyboardInterrupt:
        break
tcp_client.close()

5、基于UDP的套接字代码示例

服务端:

from socket import *

ip_port = ("192.168.1.2", 8000)
buffer_size = 1024

udp_server = socket(AF_INET, SOCK_DGRAM)  # 创建一个服务器的套接字,SOCK_DGRAM表示数据报
udp_server.bind(ip_port)  # 把ip地址和端口号绑定到套接字

while True:
    data, addr = udp_server.recvfrom(buffer_size)   #从客户端接收数据
    print(data)
    print(addr)
    udp_server.sendto(data.upper(),addr)    #向客户端发送数据
udp_server.close()  # 关闭服务器套接字

客户端:

from socket import *
ip_port = ("192.168.1.2",8000)
buffer_size = 1024

udp_client = socket(AF_INET,SOCK_DGRAM)

while True:
    msg = input(">>>").strip()
    udp_client.sendto(msg.encode("utf-8"),ip_port)  #向服务端发送数据
    data,addr=udp_client.recvfrom(buffer_size)  #从服务端接收数据
    print(data.decode("utf-8"))

udp_client.close()  # 关闭客户端套接字

三、小实验

1、基于UDP创建时间同步服务器

服务端:

from socket import *
import time

ip_port = ("192.168.1.2", 8000)
buffer_size = 1024

udp_server = socket(AF_INET, SOCK_DGRAM)  # 创建一个服务器的套接字,UDP选SOCK_DGRAM,表示数据报
udp_server.bind(ip_port)  # 把ip地址和端口号绑定到套接字

while True:
    data, addr = udp_server.recvfrom(buffer_size)
    print(data)
    if not data:
        fmt = "%Y-%m-%d %X"
    else:
        fmt = data.decode("utf-8")
    back_time = time.strftime(fmt)

    udp_server.sendto(back_time.encode("utf-8"), addr)
udp_server.close()

客户端:

from socket import *
ip_port = ("192.168.1.2", 8000)
buffer_size=1024

udp_client=socket(AF_INET,SOCK_DGRAM)

while True:
    msg = input(">>>").strip()
    udp_client.sendto(msg.encode("utf-8"),ip_port)

    data,addr = udp_client.recvfrom(buffer_size)
    print("ntp服务器的标准时间是",data.decode('utf-8'))

udp_client.close()

2、基于UDP模拟xshell

服务端:

from socket import *
import subprocess

ip_port = ("192.168.1.2",8000)
back_log = 5
buffer_size = 1024

udp_server = socket(AF_INET,SOCK_DGRAM)
udp_server.bind(ip_port)

while True:
    cmd,addr = udp_server.recvfrom(buffer_size)

    res = subprocess.Popen(cmd.decode("utf-8"),shell=True,
                           stderr=subprocess.PIPE,
                           stdout=subprocess.PIPE,
                           stdin=subprocess.PIPE)
                           #这三个PIPE的内存空间是不同的
    err = res.stderr.read()
    if err:
        cmd_res = err
    else:
        cmd_res = res.stdout.read()
    '''
    subprocess.Popen的结果的编码是以当前所在的系统为准的,如果是windows,
    那么res.stdout.read()读出的就是GBK编码的字节流,在接收端需要用GBK解码
    '''

    if not cmd_res:
        cmd_res = "执行成功".encode("utf-8")

    udp_server.sendto(cmd_res,addr)

客户端:

from socket import *
ip_port = ("192.168.1.2",8000)
back_log = 5
buffer_size = 1024

udp_client = socket(AF_INET,SOCK_DGRAM)

while True:
    cmd = input(">>>").strip()
    if not cmd: continue
    if cmd == "q": break

    udp_client.sendto(cmd.encode("utf-8"),ip_port)
    cmd_res,addr = udp_client.recvfrom(buffer_size)
    print("命令的执行结果是:",cmd_res.decode("utf-8"))

udp_client.close()

四、粘包

1、什么是粘包

由于套接字的接收端是从自己的缓存中取数据,但是它并不知道取多少数据,造成前后几次的数据混在一起。

2、产生粘包的情况

(1)在发送端,发送的数据比较小,间隔比较短,tcp协议会对其进行优化,在缓冲中,合并成一个包,一起发送。
(2)在接收端,数据没有被取完,缓冲区剩下的数据和下一次的数据混在一起。
(3)只有TCP协议会存在粘包现象,因为它是面向流的,各条数据之间无边界。UDP并不会产生粘包,因为它是面向消息的,接收端缓冲区采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),即面向消息的通信是有消息保护边界的。
客户端:

from socket import *
ip_port = ("127.0.0.1",8000)

tcp_client = socket(AF_INET,SOCK_STREAM)

tcp_client.connect(ip_port)

tcp_client.send("hello".encode("utf-8"))
tcp_client.send("world".encode("utf-8"))
tcp_client.send("CRTao".encode("utf-8"))

tcp_client.close()

服务端1:

from socket import *
ip_port = ("127.0.0.1",8000)
back_log = 5
buffer_size = 1024

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

conn,addr = tcp_server.accept()

data1 = conn.recv(1024)
print("data1",data1)

data2 = conn.recv(5)
print("data2",data2)

data3 = conn.recv(5)
print("data3",data3)

conn.close()
tcp_server.close()

'''输出结果
data1 b'helloworldCRTao'
data2 b''
data3 b''
'''

服务端2:

from socket import *
ip_port = ("127.0.0.1",8000)
back_log = 5
buffer_size = 1024

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

conn,addr = tcp_server.accept()

data1 = conn.recv(1)
print("data1",data1)

data2 = conn.recv(5)
print("data2",data2)

data3 = conn.recv(5)
print("data3",data3)

conn.close()
tcp_server.close()

'''输出结果
data1 b'h'
data2 b'ellow'
data3 b'orldC'
'''

3、粘包的解决方法

可以在数据前加个包头,写明数据长度。
服务端:

from socket import *
import subprocess
import struct
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

while True:
    conn,addr=tcp_server.accept()
    print('新的客户端链接',addr)
    while True:
        #收
        try:
            cmd=conn.recv(buffer_size)
            if not cmd:break
            print('收到客户端的命令',cmd)

            #执行命令,得到命令的运行结果cmd_res
            res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                                 stderr=subprocess.PIPE,
                                 stdout=subprocess.PIPE,
                                 stdin=subprocess.PIPE)
            err=res.stderr.read()
            if err:
                cmd_res=err
            else:
                cmd_res=res.stdout.read()

            #发
            if not cmd_res:
                cmd_res='执行成功'.encode('gbk')

            length=len(cmd_res)

            data_length=struct.pack('i',length) # i表示整型,固定4个字节。struct.pack将整型length直接打包成字节流形式
            conn.send(data_length)
            conn.send(cmd_res)
        except Exception as e:
            print(e)
            break

客户端:

from socket import *
import struct
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

while True:
    cmd=input('>>: ').strip()
    if not cmd:continue
    if cmd == 'quit':break

    tcp_client.send(cmd.encode('utf-8'))


    #解决粘包
    length_data=tcp_client.recv(4) # 取数据包头
    length=struct.unpack('i',length_data)[0]

    recv_size = 0
    recv_msg = b''
    while recv_size < length:
        recv_msg += tcp_client.recv(buffer_size)    #取实际的数据
        recv_size = len(recv_msg)

    print('命令的执行结果是 ',recv_msg.decode('gbk'))
tcp_client.close()

五、补充

客户端套接字函数:
s.connect_ex() :connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数:
s.sendall() :发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)

s.setsockopt() :设置指定套接字的参数
例如,重启服务端时可能会遇到报错:Address already in ues,这个是由于服务端仍然存在四次挥手的time_wait状态在占用地址。解决方法:

#加入一条socket配置,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #这句要在bind前添加加
phone.bind(('127.0.0.1',8080)

六、小实验:通讯前验证客户合法性

服务端:

from socket import *
import hmac,os

secret_key = b"lumaxia"
def conn_auth(conn):
    '''
    认证客户端链接
    '''
    print("开始验证新链接的合法性")
    msg = os.urandom(32) # 成一个包含32字节随机数据的字节串
    conn.sendall(msg)
    h = hmac.new(secret_key, msg, "sha256") # 创建了一个HMAC对象,第一个参数为加盐,第三个参数为指定加密算法
    digest = h.digest() # 获取计算得到的HMAC值
    respone = conn.recv(len(digest))
    return hmac.compare_digest(respone,digest)  # 比较两个哈希值是否相等
def data_handler(conn, bufsize=1024):
    '''
    处理数据
    '''
    if not conn_auth(conn):
        print("该链接不合法,关闭")
        conn.close()
        return
    print("链接合法,开始通信")
    while True:
        data = conn.recv(bufsize)
        if not data:break
        conn.sendall(data.upper())

def server_handler(ip_port, bufsize, backlog=5):
    '''
    只处理链接
    '''
    tcp_socket_server = socket(AF_INET, SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(backlog)
    while True:
        conn,addr = tcp_socket_server.accept()
        print("新链接[%s:%s]" %(addr[0], addr[1]))
        data_handler(conn, bufsize)

if __name__ == "__main__":
    ip_port = ("127.0.0.1",8000)
    bufsize = 1024
    server_handler(ip_port,bufsize)

客户端1(合法):

from socket import *
import hmac,os

secret_key = b"lumaxia"
def conn_auth(conn):
    '''
    验证客户端到服务端的链接
    '''
    msg = conn.recv(32)
    h = hmac.new(secret_key,msg,"sha256")
    digest = h.digest()
    conn.sendall(digest)

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client = socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    conn_auth(tcp_socket_client)

    while True:
        data = input(">>>").strip()
        if not data:continue
        if data == "quit":break

        tcp_socket_client.sendall(data.encode("utf-8"))
        respone = tcp_socket_client.recv(bufsize)
        print(respone.decode("utf-8"))
    tcp_socket_client.close()

if __name__ == "__main__":
    ip_port = ("127.0.0.1",8000)
    bufsize=1024
    client_handler(ip_port,bufsize)

客户端2(不合法,没有验证函数):

from socket import *

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client = socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    while True:
        data = input(">>>").strip()
        if not data:continue
        if data == "quit":break

        tcp_socket_client.sendall(data.encode("utf-8"))
        respone = tcp_socket_client.recv(bufsize)
        print(respone.decode("utf-8"))
    tcp_socket_client.close()

if __name__ == "__main__":
    ip_port = ("127.0.0.1",8000)
    bufsize=1024
    client_handler(ip_port,bufsize)

客户端3(不合法,加盐错误):

from socket import *
import hmac,os

secret_key = b"lumaxia111"
def conn_auth(conn):
    '''
    验证客户端到服务端的链接
    '''
    msg = conn.recv(32)
    h = hmac.new(secret_key,msg,"sha256")
    digest = h.digest()
    conn.sendall(digest)

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client = socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    conn_auth(tcp_socket_client)

    while True:
        data = input(">>>").strip()
        if not data:continue
        if data == "quit":break

        tcp_socket_client.sendall(data.encode("utf-8"))
        respone = tcp_socket_client.recv(bufsize)
        print(respone.decode("utf-8"))
    tcp_socket_client.close()

if __name__ == "__main__":
    ip_port = ("127.0.0.1",8000)
    bufsize=1024
    client_handler(ip_port,bufsize)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值