最近学习python3下的安全工具开发,想自己写一个梯子。为了突破某墙的封锁,需要自己创建一个加密协议。也许你使用过requests,urlib,使用这两个东西能个便捷的使用应用层的HTTP协议进行数据传递。但自己实现一个协议,却比HTTP更加底层。请参考下面的图:

从图中可以看到,应用层的下一层是传输层,socket介于应用层和传输层之间。这什么意思呢?

socket是操作系统开放的网络编程接口,在传输层之上意味着我们不用关心具体的传输细节,比如数据包太大的时候如何分片,传输的时候如何进行错误校验保证可靠性,数据丢包了如何重传,以及如何控制网络拥塞。像这些东西,都都是属于TCP/IP协议栈内容,是由传输层实现的功能,整个协议栈都由操作系统给我们封装好了。使用Python3的socket,实际上就是在使用这个操作系统提供的网络接口。

socket由于在应用层之下,所以使用socket操作的不是任何一个协议。比如requests和urlib,你使用这两个库传输的内容都是http,地址都必须是http或https开头,但socket传输的却是整个协议的比特数组。这意味着你也能把socket当requests库用,但传输的内容是整个协议的数据,你必须自己做解析http数据的工作。

说了这么多,如果你还不理解,我想用一句话概括。使用socket能够很简单方便快捷的从联网的两台计算机之间传输比特数据。除此之外,你不用再去考虑其他细节了。

使用TCP/IP传输数据的时候,是使用的C/S架构。C是客户端,S是服务端,在传输数据的过程中,始终有一方是先发起连接,另一方再接受连接。如果把传输数据的行为比作打电话,始终都会有一方会拨出电话,另一方接听电话。那么,发起连接和拨打电话的一方是客户端,接听电话和接受连接的一方的服务端。

要使服务端在任何情况下都能接受连接,需要对端口进行监听。换句话说,要实现在任何时候都能接听电话,需要派个人对电话进行值守。监听就是值守电话的过程。

我们先来看看服务端的代码,要使用socket,需要先导入socket模块。直接使用import socket导入即可。然后我们需要初始化一个socket,然后使用listen方法对端口进行监听,监听的时候我们需要一个IP和一个端口组成的元组,最后我们就可以使用accept接受连接了。

accept是一个阻塞方法,阻塞的意思有些像你开车等红绿灯。当红灯亮起的时候,你会把车停下来等待,一直等到红灯变成绿灯。那么accept方法跟这个差不多,当程序执行到accept方法的时候,会判断当前是否有人进行连接,如果没有人连接,就一直等待。一旦有人连接了,则执行后面的代码。用红绿灯的例子来说明accept方法的话,没人链接的时候就是红灯,需要一直等待,一旦有人连接,就是绿灯了,程序继续执行。

通过accept方法,我们能够拿到两个对象,一个sock,一个addr。socks是指的进行连接的客户端,addr是指的进行连接的客户端的IP和端口。

使用sock的recv和send方法,我们就能进行接收和发送数据了。recv方法的参数是一个int,代表接收多少个长度的byte。send方法的参数则是我们需要发送的byte。

在python3中,我们可以使用str的encode把一个字符串编码为byte,使用byte的decode方法把byte解码为字符串。

完整的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# coding=utf-8

'''
Created on 2021年4月13日
@author: Jason Wang

'''

import socket

if __name__ == "__main__":
print("This is server")
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 8000))
server.listen()
sock, addr = server.accept()
send_data = "Hello world"
sock.send(send_data.encode("utf-8"))
recv_data = sock.recv(1024)
print(recv_data.decode("utf-8"))
#server.close()

这里说一下第初始化socket的几个参数,socket.AF_INET代表ipv4,如果是ipv6,可以使用socket.AF_INET6。socket.SOCK_STREAM代表TCP,如果要使用UDP,可以使用socket.SOCK_DGRAM。

我们来看看客户端。同服务端基本一样,不同的是客户不用绑定端口并监听,直接使用connect对服务端进行连接。连接后就能使用send和recv进行发送和接收数据了。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# coding=utf-8
'''
Created on 2021年4月13日

@author: Jason Wang
'''
import socket

if __name__ == "__main__":
print("This is client")
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8000))
recv_data = client.recv(1024)
print(recv_data.decode("utf-8"))
client.send("I've got your message".encode("utf-8"))

上述代码中,我们连接到了127.0.0.1的8000端口,并接受了来自服务器端的数据。recv也是一个阻塞方法,如果没有数据会暂停等待数据,有数据则继续执行。接收到数据后,紧接着我们又向服务端发送了一个数据。

我们来执行一下代码,先把服务端运行起来,然后再执行客户端。服务端输出:

1
2
This is server
I've got your message

客户端输出为:

1
2
This is client
Hello world

可以看到,双方都接收到了来自对方的数据。

到这里就结束了吗?显然还没有。因为我们没有考虑来自多个连接的情况。我们现在的传输是一对一的,但在实际的运用中,却是一对多的。比如一个网站服务器监听80端口,却可以同时处理来自多个浏览器的访问。我们可以使用多线程来进行改造。每个请求都由一个线程进行处理。

示例代码:

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
# coding=utf-8
'''
Created on 2021年4月13日

@author: Jason Wang
'''
import socket, threading

def handle(sock, addr):
send_data = "Hello world"
sock.send(send_data.encode("utf-8"))
recv_data = b""
while True:
tmp = sock.recv(1024)
if tmp:
recv_data += tmp
else:
break
print(recv_data.decode("utf-8"))

if __name__ == "__main__":
print("This is server")
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 8000))
server.listen()
while True:
sock, addr = server.accept()
handle_thread = threading.Thread(target=handle, args=(sock, addr))
handle_thread.start()
#server.close()

关于多线程的内容,暂不再赘述。上述代码便是一个多线程处理客户端请求的示例代码。

最后,当不再使用socket,可以使用close方法关闭连接。任何时候,都应该及时关闭不用的socket。