加密项目开发笔记(3)——服务器

1、准备

    由于事先确定了本次项目使用Socket来进行服务端与不同客户端的通信,所以找寻相应的资料后决定PC客户端和服务端都使用了基于Python编写的普通socket连接,而初步确定的手机客户端——微信小程序使用WebSocket协议进行通信。

    原本以为Socket与WebSocket是同一种东西在不同平台的不同叫法,结果发现Socket实际上可以说是一种通信方式,而WebSocket却是一种完善的应用层协议。虽然说这个项目使用Socket来进行通信真的是杀鸡用宰牛刀,但是想着挑战杯本身就是一个学习途径,并不要求做到太过精确,所以后面遇到诸多困难后还是选择使用了Socket。

 

2、服务端的配置

    在服务端上考虑到需要进行并发处理多个Socket连接,使用了Python库SocketServer来处理多个Socket请求,SocketServer的技术核心在于使用了Select模块进行套接字的资源分配选择以及线程池的应用。

    SocketServer的功能实现上都被模块化,需要注意的是要使用多线程处理Socket首先需要定义一个继承于SocketServer.BaseRequestHandler的类,再去重写其Handle函数,此函数将作为线程的执行函数。然后实例化一个服务对象,将之前定义的类作为第二个参数,第一个参数则是由服务器的IP和Port组成的Tuple。

server = SocketServer.ThreadingTCPServer(('127.0.0.1',Port),MyServer)

随后调用对象的server_forever函数来开启select的循环,开始监听Socket请求

至于Handle函数内,可以使用self.request来获取当前连接上的客户机套接字,此后即可进行Socket通信

3、PC端通信

    PC端的通信使用的是和服务端相同的普通Socket,所以在功能实现上没有出现难以解决的问题。

    其基本使用是先实例化socket.socket对象,然后用connect函数向目标服务器发送Socket连接请求。

    连接成功后即可通信,通信中常用的是send函数和recv函数,需要注意的是发送前都需要将发送内容编码成Byte型数据,接收到的信息也都需要先进行解码操作,因为数据在网络中的传输都是以比特流形式。

4、移动端通信

    移动端通信可以说是最麻烦的一环了,因为移动端首先选择了小程序作为客户端,而小程序使用的应该是web开发那一套语言,所以其只支持HTTP和WebSocket通信,而且小程序的通信规范性完全不是我这种小渣渣能想象的。。。

    在没有多少了解WebSocket的情况下,我们首先准备使用Socket连接那一套尝试连接两者,发现总是出现无法解码的情况,只在小程序端使用了ws协议连接才会出现正常的HTTP请求头数据

    后面发现是因为,wss协议的连接是需要让服务器实现与客户端进行ssl协议的信息以及秘钥的交换,而要让服务器能够处理ssl协议一个办法是在服务器的Nginx中配置,看了一堆文章后可以总结为一个字 麻烦,不仅需要向服务器商申请ssl证书,还需要一个已经备案的域名。还有一个办法是在程序内配置,不过还没有去尝试,总感觉不会那么容易。

    然后得到客户端的HTTP请求后我查阅了许多资料,了解了很多关于WebSocket协议的东西,了解到WebSocket协议的连接建立方式是先进行HTTP握手,然后双发都同意后才开启一个socket连接,并且在连接后的数据通信中,都是需要遵循一定的数据发送规则的,后来我找到了一篇详尽的WebSocket说明——WebSocket全解

   1.WebSocket握手协议

        然后首先我准备的是握手协议以及相关实现,在此我找到了一篇JAVA服务端的Socket与web端的WebSocket进行强行通信的文章,给我带来了许多启发,握手首先接到未被加密的HTTP请求后,需要从中使用正则提取出其SEC_WEBSOCKET_KEY,是一段随机字符串,然后在这段字符串的结尾添加一段GUID,这一段GUID的值是固定的(是一段不会在通信中出现的一段字符串)——258EAFA5-E914-47DA-95CA-C5AB0DC85B11;然后对得到的字符串进行SHA-1编码再进行base64编码,需要注意str和byte型数据的转换,在这过程中掉进了一个大坑,使用Python的SHA-1编码时使用了hashlib的sha1的hexdigest进行编码,然而实际上规定是需要用digest的,使用hexdigest我猜测是先将需要编码的数据hex化了,后者对编码后的数据hex化了,(其中hex就我所知主要是转换为16进制),因此经处理后的字符串总是无法通过握手协议,于此我也发现了文章中计算出的结尾是一个=号,而我的有两个,并且偏长,然而在我使用了一些在线的sha1编码发现其也是经过hex化的数据,随后我更改了计算sha1的函数后得到了正确的ACCEPT握手码,不得不说Python的简洁,整个解密就写了几句:

def getWSResponse(request):
	pattern = re.search(r"Sec-WebSocket-Key: (\S+)", request)
	key = pattern.group(1) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
	key = sha1(key.encode("utf-8")).digest()
	key = base64.b64encode(key)
	response = ("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "
			+ key.decode("utf-8") + "\r\n\r\n")
	return response

得到accept码后再添加进websocket握手的回应中,发送回应,至此成功建立了WebSocket连接。

   2.数据的收发

        WebSocket的数据收发是需要遵守一定的协议的,所以使用纯Socket的服务端也需要对发送的和收到的数据做一定的处理。

        首先是接收的websocket数据:

解析接收的客户端信息

接收到客户端数据解析规则如下:

1byte
bit: frame-fin,x0表示该message后续还有frame;x1表示是message的最后一个frame
3bit: 分别是frame-rsv1、frame-rsv2和frame-rsv3,通常都是x0
4bit: frame-opcode,x0表示是延续frame;x1表示文本frame;x2表示二进制frame;x3-7保留给非控制frame;x8表示关 闭连接;x9表示ping;xA表示pong;xB-F保留给控制frame
2byte
1bit: Mask,1表示该frame包含掩码;0,表示无掩码
7bit、7bit+2byte、7bit+8byte: 7bit取整数值,若在0-125之间,则是负载数据长度;若是126表示,后两个byte取无符号16位整数值,是负载长度;127表示后8个 byte,取64位无符号整数值,是负载长度
3-6byte: 这里假定负载长度在0-125之间,并且Mask为1,则这4个byte是掩码
7-end byte: 长度是上面取出的负载长度,包括扩展数据和应用数据两部分,通常没有扩展数据;若Mask为1,则此数据需要解码,解码规则为1-4byte掩码循环和数据byte做异或操作。

        据上所述,我只需先关注于第2byte之后的数据,因为小程序与服务器之间的通信没有超过125的数据被传输,这里第3-6byte一般是数据掩码(经证实小程序发的websocket消息第二byte的第一位一般是1);收到信息后首先将3-6比特储存起来,然后按照一定的规律对后面的数据进行分段的解密,我的解密如下:

def getData(bdata):
	mask = bdata[2:6]
	data = ''
	n = 0
	for c in bdata[6:]:
		data += chr(mask[n%4]^c)
		n += 1
	return data

        从中其实我发现一个有趣的现象,就是将Bytes数据进行单字符切片后,提取的字符会转化为对应ascii码的int类型,这样切片后就不用对数据进行数据类型转化了,可以直接用掩码对数据进行异或运算。

        然后是发送的数据,发送的数据不需要进行掩码处理的,比较简单:

发送数据至客户端


服务器发送的数据以0x81开头,紧接发送内容的长度(若长度在0-125,则1个byte表示长度;若长度不超过0xFFFF,则后2个byte 作为无符号16位整数表示长度;若超过0xFFFF,则后8个byte作为无符号64位整数表示长度),最后是内容的byte数组。

        由于我没有太长的数据要发送所以就用\x81+长度+bytes串:

def staData(str):
	return b'\x81' + chr(len(str)).encode() + str.encode()

         至此,数据的收发问题就解决了。

    3. 用户信息处理

        本以为完成了websocket的通信问题整个功能就实现了,想着小程序可以直接将用户的唯一标识符发来然后一切OK,结果还是太Naive,小程序的规范性还真是强,感觉自己就像个乞丐程序员。。。查官方文档知道了要想拿到用户的唯一标识符openid必须要先做登陆,心累。

        看了一些官方文档后,对登陆也有了一定的了解,主要就是一句话——要把小程序用户蒙在鼓里,不能让小程序端的使用者拿到敏感数据,所以小程序要登陆时先要向微信服务器请求到一段加密了的code,把code发给开发者服务器后,开发者服务器再将code和小程序信息发给微信服务器来表明身份,然后微信服务器会将会话密钥和openid发过来,当然,是加过密的。由此得到了当前登陆用户的openid,拿着这个openid先检索数据库是否有此人信息,没有就将其数据添加进数据库中,并给他分配密钥。再然后用户扫码后小程序发送请求号到服务器,服务器检索此请求号是否还在请求队列当中,在的话就将此用户的密钥发给PC端,以此完成这个项目的最后一步功能实现。

 

5、总结

    本来在做PC的Socket的时候觉得服务器端是最容易搞的,直到我了解到了WebSocket。。服务器端应该还是最麻烦的,一个原因是需要一直与小程序端的队友远程交流,有些想要调试的功能不能及时实现,然后就是无法跟小程序端的队友实现想法的同步,本来是想找个时间集中做实验的,但是还是觉得可能都在各自写代码,交互不强。还有就是小程序编程的规范程度很高,对于我们这种半吊子来说简直就是噩梦。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值