Socket是网络编程的一个非常重要的基础,而Python的Socket标准库也提供了非常完善且易用的语法,这篇文章简单介绍一下Socket库的基本用法,以及结合os标准库,threading多线程库实现远程SSH通信,最后模拟TCP代理交互的过程

Socket库基本用法

Socket是任何一种计算机网络通讯最基础的内容,例如当你在浏览器地址栏中输入一个网址时,你就会打开一个套接字,任何连接到指定的网址并读取响应的页面然后显示出来,在Python中使用Socket库进行通信的对象无非就是两个,一个是服务器端,一个是客户端

服务器端的主要流程:

(1)初始化socket(),创建套接字

1
2
import socket
s = socket.socket(AF_INET,SOCK_STREAM)

这里socket函数是初始化socket,里面的参数默认是AF_INET和SOCK_STREAM

(2)使用bind()绑定服务器端的ip和端口

1
s.bind(('127.0.0.1',6666))

这里注意传入的参数是一个元组,包含了ip地址和端口号

(3)使用listen()监听消息

1
s.listen(5)

这里的参数最大值为5

(4)使用accept()获取客户端的套接字地址

1
sock,addr = s.accept()

这个函数有点类似input函数,input函数是等待用户输入才会执行下一步,而accept函数则等待用户的连接才执行下一步,返回也是一个元组(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据,address是连接的客户端地址

(5)使用recv()接收数据,send()发送数据与客户端进行交互

1
2
text = sock.recv(1024)
sock.send(data)

这里要注意的是在Python 3中send()中的参数必须是字节类型

客户端的主要流程:

(1)初始化socket(),创建套接字

1
c = socket.socket(AF_INET,SOCK_STREAM)

(2)使用connect()指定连接的ip和端口号连接至指定服务器端

1
c.connect(('127.0.0.1',6666))

同样的,参数必须是元组,包括要连接的服务器的ip地址和端口号

(3)使用recv()接收数据,send()发送数据与服务器端进行交互

1
2
text = c.recv(1024)
c.send(data)

其实使用socket库其实就只需要注意send()与recv()需要对应起来,不然可能导致客户端或者服务器端处于挂起的状态

下面我们就来看一个简单的服务器端和客户端的一个例子

Server(服务器端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from socket import *

s=socket(AF_INET,SOCK_STREAM) #初始化socket,创建套接字
s.bind(('localhost',6666)) #绑定服务器ip地址和端口
s.listen(5) #开始监听
while True:
sock,addr=s.accept() #等待客户端连接,获取客户端套接字地址
print('Connected By',addr)
sock.send(str.encode('Welcome to Server!')) #向客户端发送数据
while True:
data=sock.recv(1024) #接收客户端发送的数据
print(bytes.decode(data))
if not data:
print('Client has lost')
break

Client(客户端)

1
2
3
4
5
6
7
8
9
10
from socket import *

c=socket(AF_INET,SOCK_STREAM) #初始化套接字
c.connect(('127.0.0.1',6666)) #连接服务器
text=c.recv(1024)
print(bytes.decode(text))
for i in range(10):
data=input()
c.send(str.encode(data))
c.close()

服务器端使用了死循环以保持一直处于监听状态,客户端发送10次信息与服务器端断开连接,之后服务器继续等待客户端连接,这个程序缺点是服务器端只能支持一个客户端连入,不能支持多个客户端同时连入

os库基本用法

Python的os库提供了各种操作系统功能的接口,通过例子简单说明一下几种常用的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os

cmd = input('Please input a command: ')
a = os.system(cmd) #执行shell命令
print(a) #返回值为0


cmd = input('Please input a command: ')
a = os.popen(cmd) #返回一个对象
print(a.read())

print(os.getcwd()) #获取当前目录
print(os.listdir(os.getcwd())) #返回指定文件夹下包含的文件或文件夹的名字的列表
os.chdir(os.getcwd()+'/test') #到达指定目录下
os.system('test.py')

下面通过os库与socket库结合模拟SSH通信

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from socket import *
import os

s = socket(AF_INET,SOCK_STREAM)
s.bind(('localhost',6666))
s.listen(5)
while True:
sock,addr = s.accept()
print('Connected By',addr)
sock.send(str.encode('Welcome to the SSHServer!'))
while True:
cmd = sock.recv(1024)
cmd = bytes.decode(cmd)
if cmd == 'exit':
print('Client has lost')
break
result = os.popen(cmd).read()
sock.send(str.encode(result))

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from socket import *
import os

c = socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',6666))
text = c.recv(1024)
print(bytes.decode(text))
while True:
cmd = input('Please input a command: ')
c.send(str.encode(cmd))
if cmd == 'exit':
c.close()
break
result = c.recv(1024)
print(bytes.decode(result))

当客户端输入exit时,断开与服务器的连接。同样的,这里的服务器端还是只能与一个客户端连接

利用threading库实现多线程连接

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from socket import *
from threading import Thread
from time import sleep
import os

def ThreadHandle(sock,addr):
while True:
cmd = sock.recv(1024)
cmd = bytes.decode(cmd)
if cmd == 'exit':
print(addr,'has lost')
break
result = os.popen(cmd).read()
sock.send(str.encode(result))

s = socket(AF_INET,SOCK_STREAM)
s.bind(('localhost',6666))
s.listen(5)
while True:
sock,addr = s.accept()
print('Connected By',addr)
sock.send(str.encode('Welcome to SSHServer!'))
t = Thread(target=ThreadHandle,args=(sock,addr))
sleep(0.1)
t.start()

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from socket import *
import os

c = socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',6666))
text = c.recv(1024)
print(bytes.decode(text))
while True:
cmd = input('Please input a command: ')
c.send(str.encode(cmd))
if cmd == 'exit':
c.close()
break
result = c.recv(1024)
print(bytes.decode(result))

服务器端使用了threading标准库的Thread函数开启了多线程,即每个连接都是一个单独的进程

模拟TCP代理

我们假设服务器与客户端因为某种原因不能直接通信,这时候就需要一个中间的代理将客户端的信息转发到服务器,再接收服务器返回的数据再转发给客户端,这就是代理的原理

如果我们把Python中一个socket代表一个连接,那么代理既要与客户端连接又要与服务器连接,就需要两个socket

我们先来编写服务器端和客户端,跟前面的例子一样,不说明了

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from socket import *
import os

s = socket(AF_INET,SOCK_STREAM)
s.bind(('localhost',6666))
s.listen(5)
while True:
sock,addr = s.accept()
print('Connected By',addr)
while True:
cmd = sock.recv(1024)
cmd = bytes.decode(cmd)
if cmd == 'exit':
print('TCPClient has lost')
break
result = os.popen(cmd).read()
sock.send(str.encode(result))

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from socket import *

HOST = '127.0.0.1'
PORT = input('Please input a PORT to connect: ')
PORT = int(PORT)

c = socket(AF_INET,SOCK_STREAM)
c.connect((HOST,PORT))
text = c.recv(1024)
print(bytes.decode(text))
while True:
cmd = input('Please input a command: ')
c.send(str.encode(cmd))
if cmd == 'exit':
c.close()
break
result = c.recv(1024)
result = bytes.decode(result)
print(result)

接下来开始编写代理,首先我们先定义建立与客户端连接的socket的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def Server(LPORT,CHOST,CPORT):
c = Client(CHOST,CPORT)
s = socket(AF_INET,SOCK_STREAM)
s.bind(('localhost',LPORT))
s.listen(5)
while True:
sock,addr = s.accept()
print('Connected By',addr)
sock.send(str.encode('Welcome to TCPProxy!'))
while True:
cmd = sock.recv(1024)
if bytes.decode(cmd) == 'exit':
print('TCPClient '+addr+' has lost')
Forward(c,cmd)
c.close()
break
print('The command from TCPClient is: '+bytes.decode(cmd))
result = Forward(c,cmd)
print('The result from TCPServer is: '+bytes.decode(result))
sock.send(result)

因为代理是接收客户端的数据再将数据转发至服务器,再把服务器返回的结果转发到客户端。所以我们不妨把代理看成是客户端的服务器,因此定义函数名为Server,等待客户端的连接,虽然它不然处理客户端发来的数据,但是它可以将数据转发服务器端让服务器端代为处理,这里的参数LPORT是代理绑定的端口号,CHOST和CPORT是传入Client函数的参数,也就是后面讲代理看作是客户端与服务器端连接。

与客户端建立连接后,接收客户端的数据打印出来后,将数据通过Forward函数转发至服务器端,当客户端输入exit命令时,代理将断开与服务器端的连接和客户端的连接并继续等待客户端的接入

接下来编写与服务器端连接的函数Client和转发数据的函数Forward

1
2
3
4
def Client(CHOST,CPORT):
c = socket(AF_INET,SOCK_STREAM)
c.connect((CHOST,CPORT))
return c
1
2
3
4
def Forward(c,cmd):
c.send(cmd)
result = c.recv(1024)
return result

参数CHOST和CPORT是服务器端的IP和端口

再编写主函数:

1
2
3
4
5
6
7
8
def main():
try:
LPORT = int(sys.argv[1])
CHOST = sys.argv[2]
CPORT = int(sys.argv[3])
Server(LPORT,CHOST,CPORT)
except:
usage()

这里用到了sys标准库的sys.argv[],简单地说就是我们在cmd中输入执行python程序时获取的参数,假如我们在cmd命令行中输入test.py aaa bbb,那么sys.argv[0]就代表了字符串’test.py’,sys.argv[1]代表了字符串’aaa’,同理sys.argv[2]代表了字符串’bbb’,所以我们要执行这个代理的程序,不仅要输入python程序的文件名,还要同时输入三个参数,否则就出错而执行usage函数,我们就定义usage函数为这个程序的用法

1
2
def usage():
print('TCPProxy.py [ListenedPORT] [ConnectedHOST] [ConnectedPORT]')

说明了执行这个代理程序应当输入的格式

下面附上代理端完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from socket import *
import sys

def usage():
print('TCPProxy.py [ListenedPORT] [ConnectedHOST] [ConnectedPORT]')

def Server(LPORT,CHOST,CPORT):
c = Client(CHOST,CPORT)
s = socket(AF_INET,SOCK_STREAM)
s.bind(('localhost',LPORT))
s.listen(5)
while True:
sock,addr = s.accept()
print('Connected By',addr)
sock.send(str.encode('Welcome to TCPProxy!'))
while True:
cmd = sock.recv(1024)
if bytes.decode(cmd) == 'exit':
print('TCPClient '+addr+' has lost')
Forward(c,cmd)
c.close()
break
print('The command from TCPClient is: '+bytes.decode(cmd))
result = Forward(c,cmd)
print('The result from TCPServer is: '+bytes.decode(result))
sock.send(result)

def Client(CHOST,CPORT):
c = socket(AF_INET,SOCK_STREAM)
c.connect((CHOST,CPORT))
return c

def Forward(c,cmd):
c.send(cmd)
result = c.recv(1024)
return result

def main():
try:
LPORT = int(sys.argv[1])
CHOST = sys.argv[2]
CPORT = int(sys.argv[3])
Server(LPORT,CHOST,CPORT)
except:
usage()

main()