【网络编程】socke编程(一)

一.客户端/服务器架构
1.硬件C/S架构(打印机)
2.软件C/S架构
C/S架构与socket的关系
socket可以完成C/S架构的开发

二.网络协议

学socket之前要了解网络协议(挺重要的) 省略。。

三.socket层

【网络编程】socke编程(一)_第1张图片

四.什么是socket

什么是socket呢?我们经常把socket翻译为套接字:
socket是在应用层和传输层之间的一个抽象层,它是一组接口,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议

因此,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的

五.socket工作流程

工作原理如下:

【网络编程】socke编程(一)_第2张图片

socket()模块函数用法:

import socket
socket.socket(socket_family,socket_type,proto=0)
#socket_family可以是AF_UNIX或者AF_INET
#socket_type可以是SOCK_STREAM或者SOCK_DGRAM
#proto一般不填,默认值为0

#获取tcp/ip套接字
tcpsock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#获取udp/ip套接字
udpsock= socket.socket(socket.AF_INET,scoket.SOCK_DGRAM)

#由于socket模块中有太多属性,因此可以使用from socket import *,能大幅度减少代码量
from socket import *
tcpsock = socket(AF_INET,SOCK_STREAM)


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

#####客户端套接字函数
s.connect() #->主动初始化TCP服务器连接

#####公共使用的函数
#TCP
s.recv()  #->接收TCP数据
s.send()  #->发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() #->发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
#UDP
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()        #->创建一个与该套接字相关的文件

六.基于TCP的套接字

tcp是基于连接的,必须先启动服务端,然后启动客户端去链接服务端

####### tcp服务端 #######
s = socket() #创建服务器套接字
s.bind()      #把地址绑定到套接字
s.listen()      #监听链接
while True:      #服务器无限循环
    c,a = s.accept() #接受客户端链接
    while True:         #通讯循环
        c.recv()/c.send() #对话(接收与发送)
    c.close()    #关闭客户端套接字
s.close()        #关闭服务器套接字

####### tcp客户端 #######
s = socket() #创建客户套接字
s.connect()  #尝试连接服务器
while True:  #通讯循环
    s.send()/s.recv() #对话(发送/接收)
s.close() #关闭客户套接字

socket通信流程与打电话流程类似,就以打电话为例来实现一个low版的套接字通信

################### tcp服务端 ###################

import socket

ip_port = ('168.252.54.8',8080)
back_log = 5
buffer_size = 1024

#买手机 -->>产生一个phone对象  第一参数是套接字的地址家族 第二个参数是TCP协议
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#绑定手机卡
phone.bind(ip_port)
#开机      #建立tcp的双向链接
phone.listen(back_log)  #backlog半链接池,存放客户端发来的链接,最多挂起5个,防止黑客攻击,可以将半链接池的容量扩大,不容易占满
print('-->>')
#电话链接,对方的手机号
conn,addr = phone.accept() #等电话 -->>>客户端的connect和服务端的accept相对应,完成tcp的三次握手,得到一个元组
#客户端和服务端都可以先发消息
print('<<--')
#收信息
msg = conn.recv(buffer_size)
print('客户端发来的信息是:',msg)
#发信息
conn.send(msg.upper())
#断开连接
conn.close()  #触发tcp的四次挥手
#关机
phone.close() #socket程序关闭

################### tcp客户端 ###################

import socket

ip_port = ('168.252.54.8',8080)
buffer_size = 1024
#买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#拨通电话
phone.connect(ip_port)  #发起tcp三次握手
#发信息
phone.send('hello'.encode('utf-8')) #bytes(data,encoding='utf-8')
#收消息
data = phone.recv(buffer_size)
print('来自服务端发送的消息:',data)
############## 正常版本 ###################

###服务端###
from socket import *
#配置参数
ip_port = ('172.0.0.8',8080)
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()  #服务端阻塞
print('双向链接是',conn)
print('客户端地址',addr)

data = conn.recv(buffer_size)
print('客户端发来的消息是:',data.decode('utf-8'))
conn.send(data.upper())

conn.close()
tcp_server.close()

###客户端###
from socket import *
#配置参数
ip_port = ('172.0.0.8',8080)
buffer_size = 1024

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

tcp_client.send('hello'.encode('utf-8'))
print('客户端发送消息了')
data = tcp_client.recv(buffer_size)
print('来自服务端发送的消息',data.decode('utf-8'))

tcp_client.close()

############## 改进版本1 ###################
#服务端和客户端增加通信循环
#客户端增加信息输入功能

###服务端###
from socket import *
#配置参数
ip_port = ('172.0.0.8',8080)
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()  #服务端阻塞
print('双向链接是',conn)
print('客户端地址',addr)

while True:
    data = conn.recv(buffer_size)
    print('客户端发来的消息是:',data.decode('utf-8'))
    conn.send(data.upper())

conn.close()
tcp_server.close()

###客户端###
from socket import *
#配置参数
ip_port = ('172.0.0.8',8080)
buffer_size = 1024

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

while True:
    msg = input('->:').strip()
    tcp_client.send(msg.encode('utf-8'))
    print('客户端发送消息了')
    data = tcp_client.recv(buffer_size)
    print('来自服务端发送的消息',data.decode('utf-8'))

tcp_client.close()

############## 改进版本2 ###################
#服务端增加链接循环
#服务端增加try..except来捕捉处理客户端关闭导致服务端异常
#客户端增加输入为空时判断continue

###服务端###
from socket import *
ip_port = ('172.0.0.8',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('双向链接',conn)
    print('客户端地址',addr)
    while True:
        try: #捕捉异常:客户端突然关闭时,服务端报的异常,循环终止
            data = conn.recv(buffer_size)
            print('客户端发来的消息是:', data.decode('utf-8'))
            conn.send(data.upper())
        except Exception as e:
            print(e)
            break
    conn.close()

tcp_server.close()

###客户端###
from socket import *

ip_port = ('172.0.0.8',8080)
buffer_size = 1024

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

while True:
    msg = input('->:').stirp()
    if not msg:continue
    tcp_client.send(msg.encode('utf-8'))
    print('客户端发送消息了')
    data = tcp_client.recv(buffer_size)
    print('来自服务端发送的消息', data.decode('utf-8'))

tcp_client.close()

tcp的三次握手和四次挥手

【网络编程】socke编程(一)_第3张图片

① 三次握手

建立双向链接:客户端发送链接请求给服务端,服务端响应给客户端,客户端建立链接,客户端建立完链接后,回给服务端,服务端建立链接,此过程客户端和服务端之间建立了双向链接
back_log也就是半链接池,存放syn请求,服务端会从里面一个个取出来
syn洪水攻击:指的是客户端一直给服务端发送syn请求,这个时候客户端都会去响应,设置的监听链接数量(back_log)是有限的,因此极大地占用了资源

② 四次挥手

断开第一条链接:客户端向服务端主动发起断开连接请求,服务端响应,客户端收到响应,断开链接
断开第二条链接:服务端向客户端主动发起断开连接请求,客户端相应,服务端收到响应,断开链接

问题:

在重启服务端时可能会报以下异常:

① 原因:由于你的服务端仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的time_wait状态的优化方法)

② 解决方法:

#加入一条socket配置,重用ip和端口
tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
tcp_server.bind('172.0.0.8',8080)
发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
vi /etc/sysctl.conf

编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
 
然后执行 /sbin/sysctl -p 让参数生效。
 
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;

net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;

net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。

net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间

七.基于UDP的套接字

### udp服务端 ###
s = socket()   #创建一个服务器的套接字
s.bind()       #绑定服务器套接字
while True:       #服务器无限循环
     d,a = ss.recvfrom()/ss.sendto() #对话(接收与发送)
 s.close()                         #关闭服务器套接字

### udp客户端 ###
c = socket()   # 创建客户套接字
while True:      # 通讯循环
    c.sendto()/cs.recvfrom()   # 对话(发送/接收)
c.close()                      #关闭客户套接字

简单示例:

###### UDP服务端 ######
from socket import *

ip_port = ('172.0.0.8',8080)
buffer_size = 1024

udp_server = socket(AF_INET,SOCK_DGRAM) #数据报式的套接字
udp_server.bind(ip_port)

while True:
    #addr是发送端,就是客户端的ip和端口
    data,addr = udp_server.recvfrom(buffer_size)
    print(data.decode('utf-8'))
    #addr是客户端的ip和端口
    udp_server.sendto(data.upper(),addr)

udp_server.close()

###### UDP客户端 ######
from socket import *

ip_port = ('172.0.0.8',8080)
buffer_size = 1024

udp_client = socket(AF_INET,SOCK_DGRAM)

while True:
    msg = input('->:').strip()
    if not msg:continue
    #ip_port是服务端ip和端口
    udp_client.sendto(msg.encode('utf-8'),ip_port) #没有链接,只能每一次都指定端口
    #addr是发送端,也就是服务端的ip和端
    data,addr = udp_client.recvfrom(buffer_size)
    print(data.decode('utf-8'))

udp_client.close()

时间服务器

###### ntp_server ######
from socket import *
import time

ip_port = ('172.0.0.8',8080)
buffer_size = 1024

ntp_server = socket(AF_INET,SOCK_DGRAM) #数据报式的套接字
ntp_server.bind(ip_port)

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

ntp_server.close()

###### ntp_client ######
from socket import *

ip_port = ('172.0.0.8',8080)
buffer_size = 1024

udp_client = socket(AF_INET,SOCK_DGRAM)

while True:
    msg = input('->:').stirp()
    # ip_port是服务端ip和端口
    if not msg:continue
    ntp_client.sendto(msg.encode('utf-8'),ip_port) #没有链接,只能每一次都指定端口
    # addr是发送端,也就是服务端的ip和端口
    data,addr = ntp_client.recvfrom(buffer_size)
    print('ntp服务器的标准时间是:', data.decode('utf-8'))

ntp_client.close()

八.粘包现象

① 基于tcp先制作一个远程执行命令的程序

#subprocess模块
import subprocess
res = subprocess.Popen(cmd.decode('utf-8'),shell=True,
                                   stderr=subprocess.PIPE,
                                   stdout=subprocess.PIPE,
                                   stdin=subprocess.PIPE)
#获得一个subprocess对象,命令暂存在管道内
#得到的结果的编码方式以当前系统为准,windows编码方式为gbk

res.stdout.read() #读取管道内的内容,gbk编码格式,且管道内容只能取一次
res.stdout.read().decode('gbk') #要用gbk解码

注意:以linux系统为例,命令ls -l ; pwd 的结果可能既有正确stdout结果,又有错误stderr结果

############## TCP服务端 ##############
from socket import *
import subprocess

ip_port = ('175.0.0.6',8080)
buffer_size = 1024
backlog = 5

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

while True:
    conn,addr = tcp_server.accept()
    while True:
        try:
            cmd = conn.recv(buffer_size)
            print('客户端发来的命令是:', cmd.decode('utf-8'))
            #执行命令,得到命令的运行结果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() #windows系统编码是gbk
            #若执行某个命令返回值为空时
            if not cmd_res:
                cmd_res = '执行成功'.encode('gbk')
            #发信息
            conn.send(cmd_res)
        except Exception:
            break
    conn.close()

tcp_server.close()

############## TCP客户端 ##############

from socket import *

ip_port = ('175.0.0.6',8080)
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'))

    cmd_res = tcp_client.recv(buffer_size)
    print('命令的执行结果是:', cmd_res.decode('gbk'))  # 编码和解码格式要一样 gbk

tcp_client.close()

程序是基于tcp的socket,在运行时会发生粘包现象(下面再分析)

② 基于udp制作一个远程执行命令的程序

############## UDP服务端 ##############

from socket import *
import subprocess

ip_port = ('175.0.0.6',8080)
buffer_size = 1024

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

while True:
    cmd,addr = udp_server.recvfrom(buffer_size)
    print('客户端发来的命令是:', cmd.decode('utf-8'))
    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')

    udp_server.sendto(cmd_res,addr)

udp_server.close()

############## UDP客户端 ##############

from socket import *

ip_port = ('175.0.0.6',8080)
buffer_size = 1024

udp_client = socket(AF_INET,SOCK_DGRAM)

while True:
    cmd = input('->:').strip()
    if not cmd: continue
    if cmd == 'quit':break
    udp_client.sendto(cmd.encode('utf-8'),ip_port)

    cmd_res,addr = udp_client.recvfrom(buffer_size)
    print('命令的执行结果是:', cmd_res.decode('gbk'))  # 编码和解码格式要一样 gbk

udp_client.close()

基于udp的socket,在运行时永远不会发生粘包,但会导致数据丢失

九.什么是粘包

先了解socket收发消息的原理

【网络编程】socke编程(一)_第4张图片

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据

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

收发信息为空时
tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住
udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头

数据可靠性对比
udp的recvfrom,一个recvfrom(x)必须对唯一一个sendto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

TCP两种情况下会发生粘包

① 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

######## 服务端
from socket import  *

ip_port = ('176.0.0.8',8060)
buffer_size = 1024
backlog = 5

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

conn,addr = tcp_server.accept()

data1 = conn.recv(buffer_size)
print('第一次数据',data1)  #第一次数据 b'helloworldgirl' -->>将客户端三次的数据一次全部接收
data2 = conn.recv(buffer_size)
print('第二次数据',data2)
data3 = conn.recv(buffer_size)
print('第三次数据',data3)

conn.close()
tcp_server.close()

######## 客户端
from socket import *

ip_port = ('176.0.0.8',8060)

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('girl'.encode('utf-8'))

tcp_client.close()

② 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

######## 服务端
from socket import  *

ip_port = ('176.0.0.8',8060)
buffer_size = 1024
backlog = 5

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

conn,addr = tcp_server.accept()

data1=conn.recv(2)  #一次没有收完整
print('第一次数据',data1)
data2=conn.recv(1024) #下次收的时候,会先取旧的数据,然后取新的
print('第二次数据',data2)

conn.close()
tcp_server.close()

######## 客户端
from socket import *

ip_port = ('176.0.0.8',8060)

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

tcp_client.send('hello my girlfriend is in school'.encode('utf-8'))
tcp_client.send('hello'.encode('utf-8'))


tcp_client.close()

UDP不会发生粘包现象

##### 服务端
from socket import  *

ip_port = ('176.0.0.8',8060)
buffer_size = 1024

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

data1,addr = udp_server.recvfrom(buffer_size) #第一次 b'hello' -->>只收到客户端发送的一条信息
print('第一次',data1)

##### 客户端
from socket import *

ip_port = ('176.0.0.8',8060)
buffer_size = 1024

udp_client = socket(AF_INET,SOCK_DGRAM)
#没有Nagle算法的优化,间隔小数据小的数据,不会合成一个大的数据块,然后发送
udp_client.sendto('hello'.encode('utf-8'),ip_port)
udp_client.sendto('girls'.encode('utf-8'),ip_port)
udp_client.sendto('world'.encode('utf-8'),ip_port)

udp_client.close()

十.解决粘包的方法

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

第一种方法:

################ 服务端 ################

from socket import *
import subprocess

ip_port = ('175.0.0.8',8090)
buffer_size = 1024
backlog = 5

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

while True:
    conn,addr = tcp_server.accept()
    print('双向链接',conn)
    print('客户端地址',addr)

    while True:
        try:
            #收信息
            cmd = conn.recv(buffer_size)
            print('客户端发来的命令是:',cmd.decode('utf-8'))
            #执行命令,得到命令的运行结果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() #默认系统编码是gbk
            #若执行某个命令返回值为空时
            if not cmd_res:
                cmd_res = '执行成功'.encode('gbk')

            ######解决粘包
            #发送数据长度给客户端
            length = len(cmd_res)
            conn.send(str(length).encode('utf-8'))
            #收个ready信息
            client_ready = conn.recv(buffer_size)
            if client_ready == b'ready':
                #发信息
                conn.send(cmd_res)
        except Exception:
            break
    conn.close()

tcp_server.close()

################ 客户端 ################

from socket import *

ip_port = ('175.0.0.8',8090)
buffer_size = 1024
backlog = 5

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 = tcp_client.recv(buffer_size)
    #发个ready信息
    tcp_client.send(b'ready')
    #字节变整形,如果需要接受的数据大于客户端缓冲区的内存大小,则不能以length作为大小传入recv()内
    length = int(length.decode('utf-8'))
    #将每次接受的数据进行拼接
    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'))  # 编码和解码格式要一样 gbk
tcp_client.close()

存在的问题:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

第二种方法:

方法:为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

struct模块
作用:可以把一个类型,如数字,转成固定长度的bytes

>>> import struct
>>> struct.pack('i',1111)
b'W\x04\x00\x00'
>>> struct.pack('i',1111111111111)
#struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
################### 定制报文头 ###################

######### 发送端 #########
import pickle,struct

fileinfo = {
    'filename':'a.txt',
    'filesize':1024
}

baotou = pickle.dumps(fileinfo) #转换成二进制
length = len(baotou) #获得报文头长度

baotou_length = struct.pack('i',length)

conn.send(baotou_length)
conn.send(baotou)

######### 接收端 #########

baotou_length = tcp_client.recv(4)
baotou_length = struct.unpack('i',baotou_length)[0] #得到元组格式

pickle_baotou = tcp_client.recv(baotou_length)
fileinfo = pickle.loads(pickle_baotou)

fileinfo['filename']
fileinfo['filesize']

解决粘包:

################## 服务端 ##################

from socket import *
import subprocess
import struct

ip_port = ('178.0.0.8',8070)
buffer_size = 1024
backlog = 5

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

while True:
    conn,addr = tcp_server.accept()
    print('双向链接',conn)
    print('客户端地址',addr)

    while True:
        try:
            #收信息
            cmd = conn.recv(buffer_size)
            print('客户端发来的命令是:',cmd.decode('utf-8'))
            #执行命令,得到命令的运行结果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() #默认系统编码是gbk
            #若执行某个命令返回值为空时
            if not cmd_res:
                cmd_res = '执行成功'.encode('gbk')

            ######解决粘包
            #发送数据长度给客户端
            length = len(cmd_res)
            data_length = struct.pack('i',length)
            conn.send(data_length)
            conn.send(cmd_res)
        except Exception:
            break
    conn.close()

tcp_server.close()

################## 客户端 ##################

from socket import *
import subprocess
import struct

ip_port = ('178.0.0.8',8070)
buffer_size = 1024
backlog = 5

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'))  # 编码和解码格式要一样 gbk
tcp_client.close()

当然也可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节

服务端:
headers={'data_size':len(cmd_res)} #字典
head_json=json.dumps(headers) #字符串
head_json_bytes=bytes(head_json,encoding='utf-8') #bytes

conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度
conn.send(head_json_bytes) #再发报头
conn.sendall(cmd_res) #在发真实的内容

客户端:
head=tcp_client.recv(4)
head_json_len=struct.unpack('i',head)[0]
head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
data_len=head_json['data_size']

补充:

基于TCP:多个客户端同时与服务端通信时,服务端只会跟一个链接进行通信,其他的链接均在等待挂起当中,不能通信
基于UDP:UDP不用建链接,可以实现并发效果,服务端可以同时与多个客户端进行通信

十一.认证客户端的链接合法性

利用hmac+加盐的方式来实现认证

##################### 服务端 #####################

from socket import *
import hmac,os

secret_key = b'gebilaowang'

def conn_auth(conn): #认证客户端链接
    print('开始验证客户端链接的合法性')
    msg = os.urandom(32)
    conn.sendall(msg)
    h = hmac.new(secret_key,msg)
    digest = h.digest()
    response = conn.recv(len(digest))
    return hmac.compare_digest(response,digest)

def data_handler(conn,buffer_size): #处理通信
    if not conn_auth(conn):
        print('该链接不合法')
        conn.close()
        return
    print('链接合法,开始通信')
    while True:
        data = conn.recv(buffer_size)
        if not data:break
        conn.sendall(data.upper())

def server_handler(ip_port,buffer_size,backlog): #处理链接
    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,buffer_size)

if __name__ == '__main__':
    ip_port = ('172.0.0.8',8090)
    buffer_size = 1024
    backlog = 5
    server_handler(ip_port,buffer_size,backlog)

##################### 客户端 #####################

from socket import *
import hmac,os

secret_key = b'gebilaowang'

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

def client_handler(ip_port,buffer_size): #处理通信和来链接
    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'))
        response = tcp_socket_client.recv(buffer_size)
        print(response.decode('utf-8'))
    tcp_socket_client.close()

if __name__ == '__main__':
    ip_port = ('172.0.0.8',8090)
    buffer_size = 1024
    client_handler(ip_port,buffer_size)

 

你可能感兴趣的:(网络编程)