Python 构建TCP通讯
TCP/IP协议
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议 不仅仅 指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议。
关于Python 的TCP协议处理
老实说python在这种偏底层的网络协议处理上并不占太大优势,他的处理速度相对于C/C++或者别的什么语言来说相对较慢,但胜就胜在简单 wwwwww
使用 Python 实现tcp通讯,一般要通过 Python 的 scoket 模块来实现。
socket 是 Python 自带的源生库,大家并不需要通过 pip 进行下载,直接在代码里 import 就行了。
Python 提供的 socket 模块, 它提供了标准的 BSD Sockets API
socket 类型
先来看一看 socket 的构造器
socket.socket([family[, type[, proto]]])
class socket(_socket.socket):
"""A subclass of _socket.socket adding the makefile() method."""
__slots__ = ["__weakref__", "_io_refs", "_closed"]
def __init__(self, family=-1, type=-1, proto=-1, fileno=None):
pass
# 以下省略
family:
意义为地址族,决定 socket 的通讯类型,类型以及描述如下
socket 地址族类型 | 地址族描述 |
---|---|
socket.AF_UNIX | 只能够用于单一的Unix系统进程间通讯 |
socket.AF_INET | 服务器之间网络通讯 |
socket.AF_INET6 | IPv6 |
type:
意义为 scoket 类型,根据不同的值构建不同的连接,类型以及描述如下
socket 类型 | 类型描述 |
---|---|
socket.SOCK_STREAM | 流式的 scoket, 处理 TCP 连接选择这种 |
socket.SOCK_DGAM | 数据报式的 socket,处理 UDP 连接选择这种 |
socket.SOCK_RAW | 原始类型的 socket,普通的套接字无法处理 ICMP、IGMP等网络报文,而 SOCK_RAW 可以;同时,SCOK_RAW 也可以处理特殊的 IPv4 报文;利用原始 socket,可以通过 IP_HDRINCL socket选项由用户构造 IP 头。 |
socket.SOCK_SEQPACKET | 可靠的连续数据包服务 |
protocol:
一般不填默认为 0。
socket 函数
1)与 HTTP 或 UDP 协议不一样, TCP 发送数据时,已建立好 TCP 连接,所以不需要指定地址。
2)服务端与客户端不能直接发送***字符串***外的数据类型。
3)可用函数如下
服务端函数
socket 函数 | 函数描述 |
---|---|
bind(address) | 将 socket 绑定到地址,在 AF_INET 地址族方式下,以元组 (host,port) 的形式表示地址 |
listen(backlog) | 开始监听 TCP 传入连接。bcaklog 指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为 1,在Win/MAC 系统下有效。Linux系统下默认最大值。 |
accept() | 接受 TCP 连接并返沪 (conn, address), conn 是新的 socket对象,可以用来接受和发送数据。 address 是连接客户端的地址。 |
客户端函数
socket 函数 | 函数描述 |
---|---|
connect(address) | 连接到address处的套接字。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。 |
connect_ex(address) | 功能与connect(address)相同,但是成功返回0,失败返回errno的值。 |
通用函数
socket 函数 | 函数描述 |
---|---|
recv(bufsize[,flag]) | 接受TCP套接字的数据。数据以 byte 式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。 |
send(byte[,flag]) | 发送TCP数据。将 byte 中的数据发送到连接的 socket。返回值是要发送的字节数量,该数量可能小于string的字节大小。 |
sendall(byte[,flag]) | 完整发送TCP数据。将 byte 中的数据发送到连接的 socket,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。 |
recvfrom(bufsize[.flag]) | 接受UDP socket 的数据。与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的 socket 地址。 |
sendto(string[,flag],address) | 发送UDP数据。将数据发送到 socket,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。 |
close() | 关闭 socket |
getpeername() | 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。 |
getsockname() | 返回套接字自己的地址。通常是一个元组(ipaddr,port) |
setsockopt(level,optname,value) | 设置给定套接字选项的值。 |
getsockopt(level,optname[.buflen]) | 返回套接字选项的值。 |
settimeout(timeout) | 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect()) |
gettimeout() | 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。 |
fileno() | 返回套接字的文件描述符。 |
setblocking(flag) | 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。 |
makefile() | 创建一个与该套接字相关连的文件 |
socket 编程
思路
应当存在一个服务端的 socket ,监听一个端口,接受从客户端建立的连接,以及接收从客户端发送而来的消息,向客户端发送消息。
应当存在一个服务端的 socket,使用一个端口,向服务端请求构建 TCP连接,并且发送消息,并且接收从服务端发送来的消息。
伪代码
服务端:
1.创建 socket, 绑定到本地的IP与端口
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8892))
2.开始监听端口
s.listen(5)
3.进入循环,不断接受客户端的连接请求
s.accept()
4.接收数据,并且发送返回信息给客户端
s.recv(1024)
s.send()
5.传输完毕或者需求完成后,关闭 socket
s.close()
客户端:
1.创建 socket,连接服务端
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect()
2.发送数据并且接收数据
data = 'data'
s.send(data)
s.recv()
3.传输完毕或者需求完成后,关闭 socket
s.close()
示意图
代码
服务端:
# server.py
import socket
class Server:
def __init__(self, family, t, port):
self.sock = socket.socket(family, t)
self.sock.bind(('0.0.0.0', port))
self.sock.listen(10)
def accept(self):
self.sock.accept()
if __name__ == '__main__':
server = Server(socket.AF_INET, socket.SOCK_STREAM, 7792)
client, client_addr = server.sock.accept()
print('来自:' + str(client_addr) + '的连接')
data = client.recv(1024)
print('收到从:' + str(client_addr) + '的消息:')
# 接收到的数据是 byte, 需要转换成 string
print(str(data, 'utf-8'))
# 发送数据则需要转换成 byte
client.send(str('你好,我已收到消息!').encode())
客户端:
# client.py
import socket
class Client:
def __init__(self, family, t, ip, port):
self.sock = socket.socket(family, t)
self.ip = ip
self.port = port
def connect(self):
self.sock.connect((self.ip, self.port))
def recv(self, size):
return self.sock.recv(size)
def send(self, byteData):
self.sock.send(byteData)
def close(self):
self.sock.close()
if __name__ == '__main__':
client = Client(socket.AF_INET, socket.SOCK_STREAM, '127.0.0.1', 7792)
client.connect()
client.send(str('你好!').encode())
data = client.recv(1024)
client.close()
print('来自服务端的消息:\n' + str(data, 'utf-8'))
运行结果
先启动服务端,再启动客户端
服务端结果:
客户端结果:
用线程来操作 socket
# 实现 tcp 请求转发
import sys, socket, time, threading, json
# 日志锁,使得日志输出线程安全
loglock = threading.Lock()
def log(msg, ip):
loglock.acquire()
try:
print('[%s]: \n[IP:%s]\n%s\n' % (time.ctime(), ip, msg.strip()))
sys.stdout.flush()
finally:
loglock.release()
# 传输类,source为来源,target为目标
# 转发原理,服务端 与 客户端建立连接, 服务端与转发目标端建立连接
# 服务端得到两个 sock,使用PipeThread 进行消息互转
class PipeThread(threading.Thread):
def __init__(self, source, target, addr):
threading.Thread.__init__(self)
self.source = source
self.target = target
self.addr = addr
def run(self):
while True:
try:
data = self.source.recv(1024)
log('datas:', self.addr)
print(data)
if not data:
break
self.target.send(data)
except Exception as e:
print(e)
break
log('PipeThread done', self.addr)
class Forwarding(threading.Thread):
def __init__(self, port, targethost, targetport):
threading.Thread.__init__(self)
self.targethost = targethost
self.targetport = targetport
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.bind(('0.0.0.0', port))
self.sock.listen(10)
def run(self):
while True:
client_fd, client_addr = self.sock.accept()
target_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
target_fd.connect((self.targethost, self.targetport))
log('new connect', client_addr)
PipeThread(target_fd, client_fd, '(' + self.targethost + ':' + str(self.targetport) + ")").start()
PipeThread(client_fd, target_fd, client_addr).start()
# two direct pipe
if __name__ == '__main__':
Forwarding(8928, '10.110.87.243', 8879).start()
sys.exit(1)