Python实现STUN+TURN+P2P聊天

作为技术验证,最近实现了一下NAT穿透,并在此基础上完成了P2P聊天的客户端(虽然只能在命令行中打字)。理论上能不论电脑处于何种类型NAT设备后,均可以实现P2P聊天。代码和使用方法参见
https://github.com/laike9m/PyPunchP2P
这篇文章主要(简单)介绍一下必要的背景知识和原理,github上已有的内容就不再说明。

什么是NAT穿透?

穿越防火墙技术

什么是STUN, TURN?

WebRTC and the Ocean of Acronyms

如何实现NAT穿透?

Peer-to-Peer Communication Across Network Address Translators

喂这就算介绍完了吗(╯‵□′)╯︵┻━┻
咳咳,总之原理部分就这样吧


PyPunchP2P工作流程

PART ONE: 连接

假定你已经运行了server.py,并让其监听1234这个端口。客户端A首先会通过从pystun里面弄出来的那部分代码检测自己的NAT类型

nat_type, _, _ = self.get_nat_type()

然后通知服务器端,发起连接请求,同时告知服务器自己的NAT类型。client.py的第三个参数是pool值,这个值是用来匹配客户端用的。如果说两个发起连接的客户端有一样的pool值,那么就认为它们是希望通信的客户端。指定的pool值也会发送给服务器。

self.request_for_connection(nat_type_id=NATTYPE.index(nat_type))

其中

NATTYPE = (FullCone, RestrictNAT, RestrictPortNAT, SymmetricNAT)

如果一切顺利,服务器接到了这个请求,那么它会保存客户端A的信息(addr, pool, nat_type),同时继续等待另一个客户端发起请求。
好,现在客户端B也发了个请求过来,并且pool值和之前相同。服务器意识到A和B希望和对方通信,于是分别把A和B的信息发给对方。显然,这就是STUN server的本职工作。

a, b = poolqueue[pool].addr, addr  
nat_type_id_a, nat_type_id_b = poolqueue[pool].nat_type_id, nat_type_id  
sockfd.sendto(addr2bytes(a, nat_type_id_a), b)  
sockfd.sendto(addr2bytes(b, nat_type_id_b), a)

至此第一部分的工作就完成了,clientA和clientB已经连接起来了,祈祷到这里一切顺利吧。

PART TWO: 通信

注意到我们之前并没有利用NAT类型信息,下面就需要了。让我们分情况看看:

至少有一方是symmetric NAT

这是最优先考虑的情况,因为symmetric NAT是最让人头大的情况。这种情况下只能通过服务器来转发消息。于是我们的服务器华丽变身为TURN server。当然,服务器不可能什么包都转发,所以这种通信方式下双方的消息带有一个msg的前缀,目的就是标识出这是希望服务器转发的消息而不是PART ONE中发起连接的那种消息。我们的服务器是不可能使用多个端口的,因为如果端口和之前建立连接时不同,那么服务器转发的消息就会直接被symmetric NAT丢弃了。既然和之前使用的是一个socket,那么标识显然是必要的。
还有个问题是转发给谁。这一点无须担心,在建立连接时服务器已经把两个client配对了,如果是从一边来的消息,它会自动转发给另一边。

不存在symmetric NAT,至少有一方是restrict NAT

这里所指的restrict NAT包含了 RestrictNAT 和 RestrictPortNAT 两种情况。这时,是restrict NAT的那一方需要做一件事,那就是持续发包!不妨称这种包为punching包,设定为0.5s一次。另一方,不管是不是restrict NAT,接到punching包之后都会自动给出回复。原理上不难理解,因为受限的一方只有持续发包,才能让NAT设备知道对方是“已知”的,而一旦接收到回复,持续发包停止,可以开始聊天。

双方都是Full Cone

这种情况简直是天堂,直接向对方发送就行了,so easy.
实际上,大部分情况都是这种。看来生活还是有希望的╮(╯▽╰)╭

大概就是这样了。~~再次声明,代码并未在真实情况下测试过,所以未必一定能正常工作。可以保证的是原理正确,以及在模拟状况下测试正常。~~目前已经测试过了,各种状况下都能正常工作,除非路由器或者防火墙被设定为阻挡来自某些IP的UDP报文,那确实无能为力了。另外,我不知道ICE是具体是怎么工作的,到处都说是对STUN+TURN的封装,难不成就和这个差不多?

Update

有同学看完文章之后发邮件问我,正好这里也可以补充说一下:

你好! 看到你的blog,想问几个关于NAT 穿透的问题。 我们现在基于局域网+websocket实现了一个聊天软件,想知道如果走互联网的话,需要穿透NAT,是不是只能通过nat穿透的这个socket通信了?还是说如果nat穿透后,两个client就可以随意通信了?

我的回答:
现在你想把局域网 websocket 聊天扩展到任意网络,我个人认为这个是不太现实的。websocket 底层是用 TCP 实现的,设计的目的并不是为了让客户端(比如浏览器)之间可以相互通信,而是客户端和服务器之间的通信。更广泛地说,要实现互联网上任意两台电脑之间的 TCP 连接,靠谱的做法只能是 UPNP,也就是各种 BT 软件的做法。虽然我的 blog 引用的那篇论文讲了 TCP 穿透,但是太复杂了。我说的 NAT 穿透其实都是针对 UDP 的。
不论是用 TCP 还是 UDP 作穿透,之后必须继续沿用那个 socket,这一点是毫无疑问的。因为穿透的第一步是获知对方公网 ip:port,而每新开一个本地 socket,它对应的公网 port 一定会变化,所以如果你新开一个 socket 的话即使原来穿透成功了也没法通信,因为公网 port 变了。

《Liar Game》连载重开啦!

from SF动漫资讯

近日,漫画家甲斐谷忍先生通过其个人推特帐号表示,其漫画著作《LIAR GAME》即将连载再开。尽管日期尚未确定,但已经让各位粉丝翘首以盼,网上众多粉丝通过推特表示支持及期待,可见该作品人气之高。

lg

《LIAR GAME》从2005年开始在漫画杂志周刊《YOUNG JUMP》上不定期连载,至今已经发售十六卷单行本,系列销售量突破五百万部。故事正如标题所言,主要讲述骗与被骗的故事,作品通过一场“欺诈游戏淘汰赛”着重从心理学和谈判手段的角度描写人性的黑暗面。该作品还曾经在日本富士电视台上两度改编为电视连续剧,电视剧男女主角分别由男星松田翔太及女星户田惠梨香饰演。不仅如此,还于2010年和2012年两度被搬上电影大荧幕,收视、票房、人气俱佳。

169话在13年2月出的,过了一两个月我发现Liar Game并且看完了169话。当时还以为不会重开连载要像猎人一样坑掉了呢,总之能重开连载实在太好了!

对于没有看过《欺诈游戏》的人,我强烈推荐去看,绝对不会后悔。这是一部适合所有人,尤其是智商较高喜欢思考的人的漫画。

Django Best Practice:How to deal with settings.py in Git

NOTE: After writing this article several months ago, I gradually realized that despite this solution works, it is error-prone, and too verbose. I'll write a new article explaing how I deal with it now.

When I started wirting my blog, one thing that confused me is different versions of settings.py. As we know, different settings.py should be kept for developement and deployment, however when it comes to using Git, things get messy. The main difficulties lies on two things:

  1. Should I upload settings.py from developement environment to Git server? If I did, does that mean I have to change the file every time after doing Git pull on deployment server?
  2. If I choose not to upload settings.py, there is a risk of losing data.

My purpose is to keep different versions of settings.py. Here's what I did:

  1. First, on the computer I do developing, commit and push everything as you normally do, including settings.py.
  2. Create a new branch called deploy on Github.
  3. Then, on deployment server, git pull from branch master.
  4. checkout deploy branch, Modify settings.py for deployment, e.g. set DEBUG=False, change MEDIA_ROOT, STATIC_ROOT, etc. The ONLY file you should change is settings.py, DO NOT TOUCH OTHER FILES !
  5. commit and push to branch deploy.
  6. Back to developing environment, type this command:
    bash git update-index --assume-unchanged my_blog/settings.py Done :)


The key part is step 5. First you should understand what git update-index --assume-unchanged is doing, basically it assumes that the file does not change. From git-scm:

--[no-]assume-unchanged
When these flags are specified, the object names recorded for the paths are not updated. Instead, these options set and unset the "assume unchanged" bit for the paths. When the "assume unchanged" bit is on, Git stops checking the working tree files for possible modifications, so you need to manually unset the bit to tell Git when you change the working tree file. This is sometimes helpful when working with a big project on a filesystem that has very slow lstat(2) system call (e.g. cifs).

This option can be also used as a coarse file-level mechanism to ignore uncommitted changes in tracked files (akin to what .gitignore does for untracked files). Git will fail (gracefully) in case it needs to modify this file in the index e.g. when merging in a commit; thus, in case the assumed-untracked file is changed upstream, you will need to handle the situation manually.

So next time you commit your work, though settings.py may has been changed, those changes won't get uploaded to Git server, which means there is no confict when you pull from master branch on deployment server no matter how many times your settings.py has been edited there. You could also achieve this by adding my_blog/settings.py to .git/info/exclude.

Finally, workflow is like this:

Coding, Push to master —> On deployment server, pull from master

Edit settings.py on deployment server (if needed) —> Push to deploy

Be careful, NEVER push to branch master from deployment server.

Now you've pushed some new commits to Github and want to apply it on your server. Here are the steps:

# step 1
make sure you're on master branch, if not, git checkout master

# step 2
$ git pull

# step 3
$ git checkout deploy

# step 4
$ git rebase master

# step 5
$ git push origin deploy

I'm open to changes and suggestions, if you have better ideas, feel free to comment.


top