首页 > 代码库 > Linux socket跨局域网聊天和文件传输

Linux socket跨局域网聊天和文件传输

一直想写一个跨局域网聊天和文件传输,以及视频聊天的软件,这两天刚好闲着没啥事就把代码写完了,代码已经上传至github:https://github.com/vinllen/chat

其实之前想法P2P模式,P2P的话必须穿透NAT,现在的NAT有4种模式:

  1. 完全圆锥型NAT
  2. 受限圆锥型NAT
  3. 端口受限圆锥型NAT
  4. 对称NAT(双向NAT)
维基百科给出的定义如下:

  • 1.Full cone NAT,亦即著名的一对一(one-to-one)NAT

一旦一个内部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有发自iAddr:port1的包都经由eAddr:port2向外发送。任意外部主机都能通过给eAddr:port2发包到达iAddr:port1

技术分享


  • 2.Address-Restricted cone NAT

一旦一个内部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有发自iAddr:port1的包都经由eAddr:port2向外发送。任意外部主机(hostAddr:any)都能通过给eAddr:port2发包到达iAddr:port1的前提是:iAddr:port1之前发送过包到hostAddr:any. "any"也就是说端口不受限制

技术分享


  • 3.Port-Restricted cone NAT
类似受限制锥形NAT(Restricted cone NAT),但是还有端口限制。

一旦一个内部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有发自iAddr:port1的包都经由eAddr:port2向外发送。一个外部主机(hostAddr:port3)能够发包到达iAddr:port1的前提是:iAddr:port1之前发送过包到hostAddr:port3.

技术分享


  • 4.Symmetric NAT(对称NAT)
每一个来自相同内部IP与端口,到一个特定目的地地址和端口的请求,都映射到一个独特的外部IP地址和端口。同一内部IP与端口发到不同的目的地和端口的信息包,都使用不同的映射
只有曾经收到过内部主机封包的外部主机,才能够把封包发回

技术分享


对于第1种特别简单,因为端口存在映射,只要把包发网出口路由的端口即可,路由会帮你转发

对于第2,3种情况,可以采用如下办法(内容来自该博客:点击打开链接):

假设网络模型如下:

技术分享

限制性锥NAT 和端口限制性锥NAT (简称限制性NAT ),穿透限制性锥NAT 会丢弃它未知的源地址发向内部主机的数据包。所以如果现在ClientA-1 直接发送UDP 数据包到ClientB-1 ,那么数据包将会被NAT-B 无情的丢弃。所以采用下面的方法来建立ClientA-1 和ClientB-1 之间的通信。

  • 1 .ClientA-1 (202.103.142.29:5000 )发送数据包给Server ,请求和ClientB-1 (221.10.145.84:6000 )通信。
  • 2. Server 将ClientA-1 的地址和端口(202.103.142.29:5000 )发送给ClientB-1 ,告诉ClientB-1 ,ClientA-1 想和它通信。
  • 3. ClientB-1 向ClientA-1 (202.103.142.29:5000 )发送UDP 数据包,当然这个包在到达NAT-A 的时候,还是会被丢弃,这并不是关键的,因为发送这个UDP 包只是为了让NAT-B 记住这次通信的目的地址:端口号,当下次以这个地址和端口为源的数据到达的时候就不会被NAT-B 丢弃,这样就在NAT-B 上打了一个从ClientB-1 到ClientA-1 的孔。
  • 4. 为了让ClientA-1 知道什么时候才可以向ClientB-1 发送数据,所以ClientB-1 在向ClientA-1 (202.103.142.29:5000 )打孔之后还要向Server 发送一个消息,告诉Server 它已经准备好了。
  • 5. Server 发送一个消息给ClientA-1 ,内容为:ClientB-1 已经准备好了,你可以向ClientB-1 发送消息了。
  • 6. ClientA-1 向ClientB-1 发送UDP 数据包。这个数据包不会被NAT-B 丢弃,以后ClientB-1 向ClientA-1 发送的数据包也不会被ClientA-1 丢弃,因为NAT-A 已经知道是ClientA-1 首先发起的通信。至此,ClientA-1 和ClientB-1 就可以进行通信了。
对于第4种情况,oh no,太难了,需要猜测端口号(博客地址:点击打开链接):
上面讨论的都是怎样穿透锥(Cone )NAT ,对称NAT 和锥NAT 很不一样。对于对称NAT ,当一个私网内主机和外部多个不同主机通信时,对称NAT并不会像锥(Cone ,全锥,限制性锥,端口限制性锥)NAT那样分配同一个端口。而是会新建立一个Session ,重新分配一个端口。参考上面穿透限制性锥NAT 的过程,在步骤3 时:ClientB-1 (221.10.145.84: ?)向ClientA-1 打孔的时候,对称NAT 将给ClientB-1 重新分配一个端口号,而这个端口号对于Server 、ClientB-1 、ClientA-1 来说都是未知的。同样, ClientA-1 根本不会收到这个消息,同时在步骤4 ,ClientB-1 发送给Server 的通知消息中,ClientB-1 的socket 依旧是(221.10.145.84:6000 )。而且,在步骤6 时:ClientA-1 向它所知道但错误的ClientB-1 发送数据包时,NAT-1 也会重新给ClientA-1 分配端口号。所以,穿透对称NAT 的机会很小。下面是两种有可能穿透对称NAT 的策略。
1.同时开放TCP ( Simultaneous TCP open )策略
如果一个 对称 NAT 接收到一个来自 本地 私有网 络 外面的 TCP SYN 包, 这 个包想 发 起一个 “ 引入” 的 TCP 连 接,一般来 说 , NAT 会拒 绝这 个 连 接 请 求并扔掉 这 个 SYN 包,或者回送一个TCP RST (connection reset ,重建 连 接)包 给请 求方。但是,有一 种 情况 却会接受这个“引入”连接。
RFC 规定:对于对称NAT , 当 这 个接收到的 SYN 包中的源IP 地址 : 端口、目 标 IP 地址 : 端口都与NAT 登 记 的一个已 经 激活的 TCP 会 话 中的地址信息相符 时 , NAT 将会放行 这 个 SYN 包。 需要 特 别 指出 的是:怎样才是一个已经激活的TCP 连接?除了真正已经建立完成的TCP 连接外,RFC 规范指出: 如果 NAT 恰好看到一个 刚刚发 送出去的一个 SYN 包和 随之 接收到的SYN 包中的地址 :端口 信息相符合的 话 ,那 么 NAT 将会 认为这 个 TCP 连 接已 经 被激活,并将允 许这 个方向的 SYN 包 进 入 NAT 内部。 同时开放TCP 策略就是利用这个时机来建立连接的。
如果 Client A -1 和 Client B -1 能 够 彼此正确的 预 知 对 方的 NAT 将会 给 下一个 TCP 连 接分配的公网 TCP 端口,并且两个客 户 端能 够 同 时 地 发 起一 个面向对方的 “ 外出 ” 的 TCP 连 接 请求 ,并在 对 方的 SYN 包到达之前,自己 刚发 送出去的 SYN 包都能 顺 利的穿 过 自己的 NAT 的 话 ,一条端 对 端的 TCP 连 接就 能 成功地建立了 。
2.UDP 端口猜测策略
同时开放TCP 策略非常依赖于猜测对方的下一个端口,而且强烈依赖于发送连接请求的时机,而且还有网络的不确定性,所以能够建立的机会很小,即使Server 充当同步时钟的角色。下面是一种通过UDP 穿透的方法,由于UDP 不需要建立连接,所以也就不需要考虑“同时开放”的问题。
为了介绍ClientB-1 的诡计,先介绍一下STUN 协议。STUN (Simple Traversal of UDP Through NATs )协议是一个轻量级协议,用来探测被NAT 映射后的地址:端口。STUN 采用C/S 结构,需要探测自己被NAT 转换后的地址:端口的Client 向Server 发送请求,Server 返回Client 转换后的地址:端口。
参考4.2 节中穿透NAT 的步骤2 ,当ClientB-1 收到Server 发送给它的消息后,ClientB-1 即打开3 个socket 。socket-0 向STUN Server 发送请求,收到回复后,假设得知它被转换后的地址:端口( 221.10.145.84:600 5 ),socket-1 向ClientA-1 发送一个UDP 包,socket-2 再次向另一个STUN Server 发送请求,假设得到它被转换后的地址:端口( 221.10.145.84:60 20 )。通常,对称NAT 分配端口有两种策略,一种是按顺序增加,一种是随机分配。如果这里对称NAT 使用顺序增加策略,那么,ClientB-1 将两次收到的地址:端口发送给Server 后,Server 就可以通知ClientA-1 在这个端口范围内猜测刚才ClientB-1 发送给它的socket-1 中被NAT 映射后的地址:端口,ClientA-1 很有可能在孔有效期内成功猜测到端口号,从而和ClientB-1 成功通信。


/************************************华丽分割线**********************************************************/
以上内容大部分参考维基百科和chengweiv5的博客,我自己也写了代码,尝试用第二种方式去穿透我们学校所在的NAT,结果悲剧了,找了好几天的问题也没找出来到底是代码的原因呢还是NAT限制为第4种的原因呢。
But项目不能这么搁浅了。我采用了另外一条曲线救国的路线,就跟现在QQ模式一样,用中转服务器进行消息转发实现。
1.首先搞了一台具有公网ip的服务器
2.配置好环境后(比如iptables等),把服务器代码放置服务器,运行
3.客户端连接后就可以发送文件和聊天了
That‘s all,就这么简单。
唯一需要说明的就是发送文件时我调用的是sendfile的系统调用,接收文件调用了Pat Patterson的包裹函数:利用splice接收文件,理论上这两个函数是直接在内核态进行文件传递,而不用在内核态和普通态进行数据的拷贝,减少时间。我测试了一下,对几M大小的文件都没问题,不过貌似文件大了貌似传不动了,目测sendfile有大小限制。。。这个就之后再改改了。
最后一点,本来还要实现视频传输的,我的想法就是用opencv调用本地摄像头,然后把图片按每秒固定帧数截下来,按文件方法传输,再在对端对图片进行拼接成视频。后来感觉有点麻烦,最后上网一查,果然我做的部分就是重造轮子的活,现在直接都有现成的文件传输协议。这部分也等以后有时间再去完善吧。
Finally, 如果大家有谁有想法,可以和我交流交流,一起学习进步

Linux socket跨局域网聊天和文件传输