1、socket编程

一、socket是什么

Socket是应用层与TCP/UDP协议族通信的中间软件抽象层,可以理解为一组接口,把复杂的TCP/UDP协议隐藏在Socket接口后面。遵循socket的规定去编程,那么写出的程序就是遵循TCP/UDP标准的。
套接字的分类:基于文件的(AF_UNIX)和面向网络的(AF_INET)
套接字地址:主机+端口号

 二、套接字工作流程

要创建套接字,必须使用socket.socket()函数

tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #基于TCP套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  #基于UDP套接字

 

  套接字对象(内置)方法:

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

客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
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服务器

ss = socket() #创建服务器套接字
ss.bind()       #把地址绑定到套接字
ss.listen()      #监听链接
inf_loop:       #服务器无限循环
    cs = ss.accept()    #接受客户端链接
    comm_loop:         #通讯循环
        cs.recv()/cs.send() #对话(接收与发送)
    cs.close()    #关闭客户端套接字
ss.close()        #关闭服务器套接字(可选)

 TCP客户端

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

 创建TCP客户端 

from socket import *
from time import ctime

HOST = '127.0.0.1'  # 本机IP
PORT = 3000  # 端口号
BUFSIZ = 1024  # 缓冲区
ADDR = (HOST, PORT)
# tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #创建TCP/IP套接字
# udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #创建UDP/IP套接字
tcpSerSock = socket(AF_INET, SOCK_STREAM)  # TCP服务器套接字
tcpSerSock.bind(ADDR)  # 将套接字绑定到服务器地址
tcpSerSock.listen(5)  # 开启TCP监听器的调用

while True:#循环建立连接
    print('waiting for connecting ....')
    conn, addr = tcpSerSock.accept()  # 等待客户端发起TCP连接
    print('...connection from :', addr)

    while True:#循环接受这个连接发来的消息
        try: # 防止客户端异常中断,不然会报错:ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
            data = conn.recv(BUFSIZ)
            if not data:break # 防止客户端正常中断,此时循环接收为空
            conn.send(('[{}] {}'.format(ctime(), data)).encode('utf-8'))  # 格式化返回客户端发送的消息
        except Exception:
            break
    conn.close()
tcpSerSock.close()

  创建TCP客户端

from socket import *

HOST = '127.0.0.1' # 服务器的主机号和端口号
PORT = 3000
BUFSIZ = 1024
ADDR = (HOST, PORT)

tcpCliSock = socket(AF_INET, SOCK_STREAM)  #TCP客户端套接字
tcpCliSock.connect(ADDR) # 主动连接到TCP服务器

while True:
    data = input('>>')
    if not data:#如果本客户端发送的消息是空的,则跳出此次循环
        continue
    tcpCliSock.send(data.encode('utf-8'))
    data = tcpCliSock.recv(BUFSIZ)
    print(data.decode('utf-8'))

tcpCliSock.close()

四、基于UDP的套接字

UDP服务器

ss = socket() #创建UDP服务器套接字
ss.bind()       #绑定服务器套接字
inf_loop:     #服务器无线循环
    cs = ss.revefrom()/ss.sendto()  #对话(发送/接受)
ss.close()

 UDP客户端 

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

 创建UDP服务器 

from socket import *

ip_port = ('127.0.0.1', 8000)
BUFSIZE = 1024
udpSerSocket = socket(AF_INET, SOCK_DGRAM)
udpSerSocket.bind(ip_port)

while True:
    msg, addr = udpSerSocket.recvfrom(BUFSIZE)
    print(msg, addr)

    udpSerSocket.sendto(msg.upper(), addr)

 创建UDP客户端

from socket import *

ip_port = ('127.0.0.1', 8000)
BUFSIZE = 1024
udpCliSocket = socket(AF_INET, SOCK_DGRAM)

while True:
    msg = input('>>:').strip()
    if not msg:continue
    udpCliSocket.sendto(msg.encode('utf-8'), ip_port)

    back_msg, addr = udpCliSocket.recvfrom(BUFSIZE)
    print(back_msg.decode('utf-8'), addr)

五、粘包

只有TCP协议才会粘包,UDP协议不会粘包。
UDP协议不用建立连接,面向消息的,既是sendto的是空消息,UDP协议也会帮你封装报头,这样对于接收端来说,就可以区分消息的边界了。即面向消息的通信是有消息保护边界的。数据可以丢失,数据是不可靠的,不会粘包。
TCP协议需要建立连接,三次握手四次挥手,面向数据流。发送发发送文件是按照一段一段的字节流发送的,接收方不知道字节流的边界。TCP协议的包不会丢失,这次没有收完包,下次接受会继续上次接受,己端总是在收到ACK确认是才会清楚缓冲区内容。数据是可靠的,但会粘包。

两种情况下会发生粘包:
(1)发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
(2)接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包) 

六、基于TCP执行远程代码(可能会有粘包)

server:

from socket import *
import subprocess

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)

while True:
    conn, addr = tcp_server.accept()
    print('新的客户端连接:', addr)
    while True:
        try:# 防止客户端异常中断
            cmd = conn.recv(buffer_size)
            if not cmd: break # 防止客户端正常中断,,此时循环接收为空
            print('收到的命令:', cmd)
            # 处理收到的命令
            res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stdin=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            err = res.stderr.read()
            if err:
                cmd_res = err
            else:
                cmd_res = res.stdout.read()
            # 发送命令结果
            conn.send(cmd_res)
        except Exception as e:
            print(e)
            break
    conn.close()

 client:

from socket import *

ip_port = ('127.0.0.1', 8000)
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'))
    # 接受命令结果
    cmd_res = tcp_client.recv(buffer_size)
    print('命令的执行结果:', cmd_res.decode('gbk'))
tcp_client.close()  # 正常关闭掉连接

七、基于UDP执行远程代码(不会有粘包)

server:

from socket import *
import subprocess

ip_port = ('127.0.0.1', 8000)
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)
    # 处理收到的命令
    res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                           stdout=subprocess.PIPE,
                           stdin=subprocess.PIPE,
                           stderr=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)

 client:

from socket import *

ip_port = ('127.0.0.1', 8000)
buffer_size = 1024

udp_client = socket(AF_INET, SOCK_DGRAM)

while True:
    cmd = input('>>:').strip()
    print('cmd', cmd)
    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'))
udp_client.close()

八、TCP解决粘包(方法一) 

server:

from socket import *
import subprocess

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)

while True:
    conn, addr = tcp_server.accept()
    print('新的客户端连接:', addr)
    while True:
        try:# 防止客户端异常中断
            cmd = conn.recv(buffer_size)
            if not cmd: break # 防止客户端正常中断,,此时循环接收为空
            print('收到的命令:', cmd)
            # 处理收到的命令
            res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stdin=subprocess.PIPE,
                                   stderr=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)
            conn.send(str(length).encode('utf-8'))#发送长度
            client_ready = conn.recv(buffer_size) # 接收client发送的ready信号,此时可以发送
            if client_ready == b'ready':
                conn.send(cmd_res)
        except Exception as e:
            print(e)
            break
    conn.close()

 client:

from socket import *

ip_port = ('127.0.0.1', 8000)
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 = tcp_client.recv(buffer_size)#接收server发送来的长度
    tcp_client.send(b'ready')#给server发送ready信号,server收到后就会发送信息
    length = int(length.decode('utf-8'))

    recv_size = 0
    recv_msg = b''#用来拼接接收的数据
    while recv_size < length: #只要收到的数据长度小于length,那么就会继续接收
        recv_msg += tcp_client.recv(buffer_size)
        recv_size = len(recv_msg)

    print('命令的执行结果:', recv_msg.decode('gbk'))#windows系统是gbk编码,不是utf-8
tcp_client.close()  # 正常关闭掉连接

 九、TCP解决粘包(方法二    利用struct模块) 

server:

from socket import *
import subprocess
import struct

ip_port = ('127.0.0.1', 8000)
back_log = 5
buffer_size = 1024

tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)#解决TCP协议中四次挥手的time_wait状态在占用地址问题
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)
            # 处理收到的命令
            res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stdin=subprocess.PIPE,
                                   stderr=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')

            # 解决粘包
            data_length = struct.pack('i', len(cmd_res))#利用struct模块得到数据长度
            conn.send(data_length) #发送长度
            conn.sendall(cmd_res)#发送真是数据内容

        except Exception as e:
            print(e)
            break
    conn.close()

 client: 

from socket import *
import struct

ip_port = ('127.0.0.1', 8000)
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'))

    # 解决粘包
    data_length = tcp_client.recv(4)
    length = struct.unpack('i', data_length)[0]#得到数据长度
    recv_size = 0
    recv_msg = b''  # 用来拼接接收的数据
    while recv_size < length:  # 只要收到的数据长度小于length,那么就会继续接收
        recv_msg += tcp_client.recv(buffer_size)
        recv_size = len(recv_msg)

    print('命令的执行结果:', recv_msg.decode('gbk'))#windows系统是gbk编码,不是utf-8
tcp_client.close()  # 正常关闭掉连接

 十、把报头做成字典,字典里包含将要发送的真实数据的详细信息(利用json模块)

 server:

from socket import *
import subprocess, struct, json

ip_port = ('127.0.0.1', 8000)
back_log = 5
buffer_size = 1024

tcp_server = socket(AF_INET, SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)#解决TCP协议中四次挥手的time_wait状态在占用地址问题
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)
            # 处理收到的命令
            res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stdin=subprocess.PIPE,
                                   stderr=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')

            # 解决粘包
            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)))#发送报头长度,用struck将报头长度这个数字转成固定长度:4个字节
            conn.send(head_json_bytes) # 发送报头的字节格式
            conn.sendall(cmd_res)#发送真实数据内容

        except Exception as e:
            print(e)
            break
    conn.close()

  client:

from socket import *
import struct, json

ip_port = ('127.0.0.1', 8000)
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'))

    # 解决粘包
    head = tcp_client.recv(4) #先收报头4个bytes,得到报头长度的字节格式,server端用struck将报头长度这个数字转成固定长度:4个字节,故此处接收4个bytes
    head_json_len = struct.unpack('i', head)[0] #利用struct提取报头的长度
    head_json = json.loads(tcp_client.recv(head_json_len).decode('utf-8')) #得到报头的字节格式,并反序列化得到报头
    data_len = head_json['data_size'] #从报头中得到真实数据长度

    recv_size = 0
    recv_msg = b''  # 用来拼接接收的数据
    while recv_size < data_len:  # 只要收到的数据长度小于length,那么就会继续接收
        recv_msg += tcp_client.recv(buffer_size)
        recv_size = len(recv_msg)

    print('命令的执行结果:', recv_msg.decode('gbk'))#windows系统是gbk编码,不是utf-8
tcp_client.close()  # 正常关闭掉连接

 

posted @ 2021-01-06 15:47  cheng4632  阅读(98)  评论(0编辑  收藏  举报