基于TCP协议通讯的套接字

什么是 Socketshell

Socket 是应用层与 TCP/IP 协议通讯的中间软件抽象层,它是一组接口。在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来讲,一组简单的接口就是所有,让 Socket 去组织数据,以符合指定的协议。编程

因此,咱们无需深刻理解 TCP/UDP 协议,socket 已经为咱们封装好了,咱们只须要遵循 socket 的规定去编程,写出的程序天然就是遵循 TCP/UDP 标准的。设计模式

套接字的分类:服务器

  基于文件类型的套接字家族:AF_UNIX(在 Unix 系统上,一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程同时运行在同一机器,能够经过访问同一个文件系统间接完成通讯)网络

  基于网络类型的套接字家族:AF_INET(Python 支持不少种地址家族,可是因为咱们只关心网络编程,因此大部分时候咱们只使用 AF_INET)并发

基于 TCP 协议的 socketssh

工做流程:socket

下面咱们举个打电话的小例子来讲明一下tcp

若是你要给你的一个朋友打电话,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就创建起了链接,就能够讲话了。等交流结束,挂断电话结束这次交谈。 生活中的场景就解释了这工做原理。ide

(若是你去一家餐馆吃饭,假设那里的老板就是服务端,而你本身就是客户端,当你去吃饭的时候,你确定的知道那个餐馆,也就是服务端的地址,可是对于你本身来讲,餐馆的老板不须要知道你的地址)

服务端
1)建立套接字描述符(socket)
2)设置服务器的 IP 地址和端口号(须要转换为网络字节序的格式)
3)将套接字描述符绑定到服务器地址(bind)
4)将套接字描述符设置为监听套接字描述符(listen),等待来自客户端的链接请求,监听套接字维护未完成链接队列和已完成链接队列
5)从已完成链接队列中取得队首项,返回新的已链接套接字描述符(accept),若是已完成链接队列为空,则会阻塞
6)从已链接套接字描述符读取来自客户端的请求(read / recv)
7)向已链接套接字描述符写入应答(write / send)
8)关闭已链接套接字描述符(close),回到第 5 步等待下一个客户端的链接请求

 服务端必须知足至少三点:

  1)绑定一个固定的 IP 和端口号

  2)一直对外提供服务,稳定运行

  3)可以支持并发

客户端:
1)建立套接字描述符(socket)
2)设置服务器的 IP 地址和端口号(须要转换为网络字节序的格式)
3)请求创建到服务器的 TCP 链接并阻塞,直到链接成功创建(connect)
4)向套接字描述符写入请求(write / send)
5)从套接字描述符读取来自服务器的应答(read / recv)
6)关闭套接字描述符(close)

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, socket.SOCK_DGRAM)
socket模块函数用法
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 1. 服务端套接字函数
phone.bind('主机ip地址', 端口号)  # 绑定到(主机,端口号)套接字
phone.listen()  # 开始TCP监听
phone.accept()  # 被动接受TCP客户的链接,等待链接的到来
服务端套接字函数
# 2. 客户端套接字函数
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 买手机
phone.connect()  # 主动链接服务端的ip和端口
phone.connect_ex()  # connect()函数的扩展版本,出错的时候返回错码,而不是抛出异常
客户端套接字函数
# 3. 服务端和客户端的公共用途的嵌套字函数
phone.recv()  # 接受TCP数据
phone.send()  # 发送TCP数据
phone.recvfrom()  # 接受UDP数据
phone.sendto()  # 发送UDP数据
phone.getpeername()  # 接收到当前套接字远端的地址
phone.getsockname()  # 返回指定套接字的参数
phone.setsockopt()  # 设置指定套接字的参数
phone.close()  # 关闭套接字
服务端和客户端的公共用途的嵌套字函数
# 面向锁的套接字方法
phone.setblocking()  # 设置套接字的阻塞与非阻塞模式
phone.settimeout()  # 设置阻塞套接字操做的超时时间
phone.gettimeout()  # 获得阻塞套接字操做的超时时间
面向锁的套接字方法
# 面向文件的套接字函数
phone.fileno()  # 套接字的文件描述符
phone.makefile()  # 建立一个与该套接字相关的文件
面向文件的套接字函数

TCP是基于连接的,必须先启动服务器,而后再启动客户端去连接服务端

简单版

import socket # 1. 建立套接字描述符, 用来创建连接
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print(phone) # 2. 设置IP和端口号, 绑定套接字描述符
phone.bind(("127.0.0.1", 8080)) # 3. 将套接字描述符设置为监听状态, 设置同一时刻最大请求数为5
phone.listen(5) print("start...") # 4. 等待来自客户端的链接
conn, client_addr = phone.accept() # accept有返回值,是一个元组 # 元组的第一个参数是双向连接的套接字对象(即三次握手的结果), 用来收发消息 # 第二个参数是一个元组,存放客户端的IP和端口号 # print(conn) # print(client_addr)

# 5. 收/发消息, 1024是接收的最大字节数bytes
data = conn.recv(1024) print("收到客户端的数据", data) conn.send(data.upper()) # 6. 关闭双向连接的套接字对象
conn.close() # 7. 关闭套接字描述符
phone.close()
服务端
import socket # 1. 建立套接字描述符
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 链接服务端的IP地址和端口号
phone.connect(("127.0.0.1", 8080)) # 3. 发/收消息
phone.send("hello".encode("utf-8"))    # 只能发bytes类型
data = phone.recv(1024) print("收到服务端的消息", data) # 4. 关闭套接字描述符
phone.close()
客户端

因为 socket 模块中有太多的属性。在这里破例使用了 'from module import *' 语句。使用 'from socket import *',就把 socket 模块里的全部属性都带到命名空间里了,这样能大幅减短代码。
例如 tcpSock = socket(AF_INET, SOCK_STREAM)

通讯循环

from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) conn, client_addr = server.accept() # 通讯循环
while True: data = conn.recv(1024) conn.send(data.upper()) conn.close() server.close()
服务端
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # 通讯循环
while True: msg = input("请输入: ").strip() client.send(msg.encode("utf-8")) data = client.recv(1024) print(data) client.close()
客户端

可是这样写有一个 bug,当你手动结束客户端的程序运行时,服务端也会跟着崩溃

由于 conn 表明的是一个双向链接,只有服务端和客户端都正常运行的时候,conn 才有意义,然而此时客户端是非正常的断开,服务端还在使用没有意义的 conn 作 recv 操做,没法收到消息,因此在 Windows 上直接崩溃,而在 Linux 上,相同的操做服务端会一直处于收空的状态

补救措施是,在 Windows 系统上捕捉异常,在 Linux 系统上加上判断

from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) conn, client_addr = server.accept() # 通讯循环
while True: try: data = conn.recv(1024) # 针对Linux系统
        if len(data) == 0: break conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
服务端
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # 通讯循环
while True: msg = input("请输入: ").strip() client.send(msg.encode("utf-8")) data = client.recv(1024) print(data) client.close()
客户端

连接通讯循环

这样虽然解决了崩溃问题,可是当手动结束客户端时,服务端仍是会跟着结束,因此在服务端等待客户端的链接前加上循环,从而达到 “连接 + 通讯” 循环

from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) # 连接循环
while True: conn, client_addr = server.accept() # 通讯循环
    while True: try: data = conn.recv(1024) # 针对Linux系统
            if len(data) == 0: break conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
服务端
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # 通讯循环
while True: msg = input("请输入: ").strip() client.send(msg.encode("utf-8")) data = client.recv(1024) print(data) client.close()
客户端

但这样作,服务端每次只能针对于一个客户端,只有当这个客户端的收发消息结束后才能给下一个客户端服务,没法达到并发的效果,这个后面学到并发时再讲

其实还有一个问题,当客户端传一个空消息时,会发生阻塞状态,由于发空的时候服务端时没法收到的(空时是什么都没有),服务端收不到,没法返回给客户端,因此客户端处于阻塞状态。补救方法是不让客户端输入空

from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # 通讯循环
while True: msg = input("请输入: ").strip() if len(msg) == "0": continue client.send(msg.encode("utf-8")) data = client.recv(1024) print(data) client.close()
客户端
from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) # 链接循环
while True: conn, client_addr = server.accept() # 通讯循环
    while True: try: data = conn.recv(1024) # 针对Linux系统
            if len(data) == 0: break conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
服务端

 模拟ssh实现远程执行命令

当使用客户端远程链接服务器时,在客户端上执行命令,服务器会返回命令执行的结果给客户端,那么该如何实现呢?

from socket import *
import subprocess server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) # 链接循环
while True: conn, client_addr = server.accept() # 通讯循环
    while True: try: cmd = conn.recv(1024)  # cmd = b'dir'
            # # 针对Linux系统
            if len(cmd) == 0: break
            # 命令的执行结果
            obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() conn.send(stdout + stderr) except ConnectionResetError: break conn.close() server.close()
服务端
import socket # 1. 建立套接字描述符
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 链接服务端的IP地址和端口号
phone.connect(("127.0.0.1", 8080)) # 3. 发/收消息
phone.send("hello".encode("utf-8"))    # 只能发bytes类型
data = phone.recv(1024) print("收到服务端的消息", data) # 4. 关闭套接字描述符
phone.close()
客户端

可是目前这样有一个局限性,我将接收端数据的最大字节数设置为1024,当发送端发的数据量小于接收端的1024时,能够被彻底接收,可是发送端的数据量大于1024时,就只能接收1024条数据,那么多出的那些数据该如何处理呢?

首先客户端发送一条执行命令给服务端,让服务端接收,这里命令的字节数大多数状况不会大于1024,因此能够被彻底接收,暂不考虑,当服务端接收了命令执行后,会将命令的执行结果发送给客户端,让客户端接收,这里命令的执行结果是颇有可能大于1024个字节的,例如:tasklist,在终端上显示的最后一条是本身,而在上面所写的两个文件中只能显示几条结果,很显然是大于1024的

但这时再输入 dir 时,居然是 tasklist 没有执行完的继续显示,再输入其它命令,仍是 tasklist 没有执行完的继续显示,这发生了什么?

这就是待解决的粘包问题,下一节将会学习

相关文章
相关标签/搜索