网游中的网络编程3:在UDP上创建虚拟链接

目录

  1. 网游中的网络编程系列1:UDP vs. TCP
  2. 网游中的网络编程2:发送和接收数据包
  3. 网游中的网络编程3:在UDP上创建虚拟链接
  4. TODO

2、在UDP上创建虚拟链接

介绍

UDP是无链接的,一个UDPsocket能够被用作,与任意数量的计算机交换数据包。然而,在多人游戏中,咱们只但愿在一小部分创建起链接的计算中,交换数据包。html

因此,咱们须要作的第一步就是:在UDP上让两台计算机,创建起虚拟链接。python

可是,首先,咱们先深刻到底层,弄清楚互联网是如何工做的。linux

互联网不是一系列的电话线

在2006年, Senator Ted Stevens作了一个互联网历史上,著名的一次演讲:git

“The internet is not something that you just dump something on. It’s not a big truck. It’s a series of tubes”程序员

当我第一次使用互联网的是:1995年,我在大学的计算机实验室中,我用Netscape浏览器上网,当时漫无目的的瞎逛。github

我当时想:每次连上一个网站,就产生一些“真实的链接”,就像电话线。我十分惊奇,当我每次访问一个新的网站的时候须要花费多少钱?(做者当时认为,每次访问网站都是创建在一条通讯线路之上,就像电话线,须要拉线)不会有人找上门,让我付这些线路的费用吧?编程

固然,这个想法如今看起来很傻。浏览器

没有直接的链接

互联中:没有一条通讯电缆,直接通讯的两台计算机。数据是由IP协议,经过数据包,从一个个电脑传递过来的。(就像传纸条)服务器

一个数据包可能经过几个计算机才能到达目的地。你不能知道准确的传递过程(第一步,第二步。。。),这个过程是会变化的,是根据网络质量决定数据包的下一步走向。你可能发送过两个数据包A和B到同一个地址,它们可能走的是不一样的路线。这个也是数据包无序的一个缘由。网络

在Linux和Unix系统上(win能够用‘tracert’),可使用‘traceroute’指令来查看数据包的传递线路和途径的主机名和IP地址。traceroute请参考

试一下traceroute指令:

traceroute: Warning: baidu.com has multiple addresses; using 220.181.57.217
traceroute to baidu.com (220.181.57.217), 64 hops max, 52 byte packets
 1  192.168.1.1 (192.168.1.1)  4.727 ms  4.960 ms  4.144 ms
 2  223.20.160.1 (223.20.160.1)  13.405 ms  6.047 ms  8.561 ms
 3  218.241.252.185 (218.241.252.185)  4.735 ms  2.130 ms  7.771 ms
 4  218.241.252.197 (218.241.252.197)  6.849 ms  5.335 ms  4.555 ms
 5  202.99.1.217 (202.99.1.217)  4.025 ms  13.324 ms  3.761 ms
 6  * 218.241.244.21 (218.241.244.21)  8.492 ms
    218.241.244.9 (218.241.244.9)  5.389 ms
 7  218.241.244.33 (218.241.244.33)  6.699 ms  4.851 ms  5.386 ms
 8  * * *

注意:第八行是由于有ICMP防火墙,请求被拒绝了,因此没有探测出目的ip地址。

这个过程就能诠释:没有直接的链接。

如何收到数据包

正如第一篇文章,举的那个简单的例子:收到数据包,就像在一个房间,人们手递手传纸条。

互联网是网络的网络(网络的集合)。固然咱们不只在一个小房子中传递信件,咱们能把它传到世界各地。

最好的例子是邮局系统!

当你想要发一封信给别人,你须要把你的信放到邮箱中,同时你会相信会到达收件人的手上。信件怎么到的,你不须要关心,反正到了。总得有人把你的信送到目的地,那么究竟是怎么送到的呢?

首先,邮递员不会拿着你的信,直接送到目的地!邮递员拿着你的信,送到当地邮局,让邮局处理。

若是这封信是本地的,本地的邮局会收过来,安排让另一个邮递员直接送到目的地。可是,若是信的地址不是本地的,那么本地的邮局不会直接把信送到目的地,因此邮局会送到上一级(镇邮局送到市邮局),或者送到临近城市的邮局,若是目的地太远就会送到飞机场。信件的传输方式是用大卡车。

咱们来看一个例子:假定一封信,从洛杉矶寄到北京,本地邮局接收到信,而后发现是国际信件,就直接送到洛杉矶的邮件中心。这封信,确认收件的地址无误,就安排到下一班飞机飞往北京的航班。

飞机着陆在北京,北京的邮件系统确定是和洛杉矶的邮件系统不同。北京的邮件中心收到这封信后,就送到具体的区级的当地邮局,最终,这封信会经过一个邮递员直接送到收件人的手里。

就像邮局系统,经过地址传递信件同样。网络传递数据包是经过IP地址。传递数据包的细节和路径选择是很是复杂的,可是基本思想:每一个路由器都是一台计算机,由路由表决定数据下一步走的地址。(这部分我省略了一些路由和路由表的部分,我没有看懂,后面研究明白,回来再补全。如今不影响后面的阅读)

编辑路由表的工做是网络管理员的工做,不是咱们这些程序员关心的问题(还好😋)。可是,若是你想了解更多关于这方面的知识,能够看看下面这些文章:

虚拟链接

如今回到链接的话题上。

若是你使用TCP socket,你知道它是面向链接的,看起来像一个‘链接’。可是TCP是创建在IP协议上的,而IP协议只是数据包在计算机之间传递(并无链接的概念),因此TCP的链接概念必定是:虚拟链接。

若是TCP能够创建再IP上创建虚拟的链接,那么咱们也能在UDP上实现虚拟链接。

让咱们定义虚拟链接:两台计算机间传输UDP数据包以固定的速度,如每秒10个包。只要数据包传输流畅,咱们就认为:两个计算机创建起了虚拟链接。

链接分为两部分:

  • 监听计算机连入,咱们称这个计算机为‘服务器’。
  • 经过IP地址和端口链接服务器的计算机,咱们成为‘客户端’。

咱们把场景设定为(先设定简单的场景,一点点来):不论什么时候,咱们只容许一个客户端链接服务器。同时,咱们假定服务器的IP地址不变,客户端是直接链接服务器。后面的文章再说支持多个客户端链接的例子等,如今先现实咱们限定条件下,简单的虚拟链接,这样能够更好的理解虚拟链接。

协议id

UDP是无链接的,UDP socket会接收任意计算机发来的数据包。

咱们将限定:服务器只从客户端接收数据包,客户端只给服务器发送数据包(一对一)。咱们不能经过地址过滤数据包,由于服务器不知道客户端的地址(python中socket能够经过recvfrom方法获得地址)。因此咱们在每一个UDP数据包加一个‘头信息’,由32位protocol id组成:

[uint protocol id]
(packet data...)

protocol id只是一些惟一的数字。若是数据包的protocol id不能匹配咱们的protocol id,数据包就被忽略。若是protocol id匹配,咱们就接收packet data。

你只须要选择惟一的数字,能够用hash你的游戏名字和协议版本数字。你也能够用任何信息当作protocol id,须要保证protocol id的惟一性,由于这个protocol id是咱们链接协议的基础。

检测链接

如今咱们须要一个检查链接的方法。

固然咱们能够作一些复杂的握手,此过程须要发送和接收多个UDP数据包。或许客户端‘请求链接’数据包,发送服务器,服务器响应返回给客户端‘链接接受’,或者若是客户端请求与,已经和其余客户端创建起链接的服务器,创建链接,则服务器就会返回给客户端‘忙碌中’。

或者,咱们可让服务器检查接收到的第一个数据包的protocol id是否正确,而后考虑是否创建链接。

客户端假定与服务器创建起链接,而后给服务器发送数据包。当服务器接受到客户端发来的第一个数据包,就记下该客户端的IP地址和端口号,最后,返回响应数据包。

客户端已经知道服务器的地址和端口。因此,当客户端接受数据包,客户端会过滤掉任何不是服务器地址的请求。一样,服务器接收到客户端的第一个数据包,经过recvfrom方法,能获取到客户端的IP地址和端口。因此服务器也能够忽略不来自指定客户端的任何数据包。

咱们可使用这个简洁的方式,由于咱们只须要在两台计算机之间创建链接。在后面的文章中,咱们会升级咱们的链接系统,用于支持两个以上的计算机链接,而且使得链接更加健壮。

(就是与特定ip地址和端口的计算机进行传输数据)

检测断开链接

咱们如何检测断开链接?

若是一个链接被定为接收数据包,那么断开链接就能够定义为不接收数据包。

为了查明咱们没有接受数据包,服务器和客户端两边都计算:从上一次接收到数据包的开始,到下一个接收到数据包的时间。(也就是所谓的‘超时时间’)

每次若是咱们接收到数据包,就重置计时器(’超时时间’清零)。若是计时器超过设定的值,侧链接‘超时’,咱们就是断开链接(再也不限制链接客户端的IP和端口)。

这也是一种优雅的方式用来处理,第二个客户端请求已创建链接的服务器的状况。创建起链接的服务器不会接收来自其余客户端的数据包,因此第二个客户端接收不到服务器响应的数据包,因此第二个客户端链接超时并处于断开链接的状态。

总结

这些就是创建虚拟链接的过程:创建链接,过滤不是来自链接的计算机的数据包,检查断开链接,设定超时。

咱们的创建的链接跟其余TCP链接同样,稳定的UDP数据包传输是多人动做游戏的基础。

目前为止,已经在UDP上创建虚拟的链接,你就可使用它来进行多人游戏中的,client/server模式下的数据传输,来替代TCP。

python实现

仍是推荐看,英文原文中的源代码

看完理论部分,下面我就用python根据上述原理实现:我写的UDP上实现虚拟链接只作了两件事:每次只能有一个socket和server进行通讯;若是在一段时间无数据传输,则注销掉原来的链接,容许创建新的链接。

注:下面代码中不少细节没有处理,仅供你们参考。

  • protocol id:我打算用时间戳hash一个字符串
  • 监测断开链接:经过settimeout()方法,捕获socket.timeout异常

test_server.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
#   Author  :   XueWeiHan
#   E-mail  :   595666367@qq.com
#   Date    :   16/5/11 下午3:54
#   Desc    :   server

import socket
import time

UDP_IP = ''
UDP_PORT = 5000
_ID = []  # 存储创建链接的protocol_id
_IP = None  # 存储创建链接的IP和端口
TIME_OUT = 2  # 超时时间(s)

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
sock.settimeout(TIME_OUT)

def check_protocol_id(protocol_id, _ID):
    '''
    检测protocol_id
    '''
    if _ID:
        if protocol_id in _ID:
            return True
        else:
            return False
    else:
        _ID.append(protocol_id)
        return True

print '准备接收内容。'
while 1:
    try:
        response = ''
        data, addr = sock.recvfrom(1024)  # 缓冲区大小为1024bytes
        protocol_id, data = data.split('|')
        if _IP:
            if _IP == addr:
                response = '创建链接'
                print '从{ip}:{port},接收到内容:{data}'.format(ip=addr[0],
                                                               port=addr[1], data=data)
            else:
                response = '没法创建链接'
        else:
            if check_protocol_id(protocol_id, _ID):
                _IP = addr
                response = '创建链接'
                print '从{ip}:{port},接收到内容:{data}'.format(ip=addr[0],
                                                               port=addr[1], data=data)
            else:
                response = '没法创建链接'
        # 返回响应数据包给客户端
        sock.sendto(response, addr)
    except socket.timeout:
        print '链接超时,注销链接,其余socket能够连入'
        _IP = None
        _ID = []

test_client.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
#   Author  :   XueWeiHan
#   E-mail  :   595666367@qq.com
#   Date    :   16/5/11 下午3:54
#   Desc    :   client
import socket
import time
import hashlib

UDP_IP = ''
UDP_PORT = 5000
MESSAGE = 'Hello, world!'
TIME_OUT = 3

print 'UDP 目标IP:', UDP_IP
print 'UDP 目标端口:', UDP_PORT
print '发送的内容:', MESSAGE

class Udp(object):
    def __init__(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.server_addr = None
        # 设置超时时间
        self.socket.settimeout(TIME_OUT)

    @property
    def protocol_id(self):
        hash = hashlib.md5(str(time.time()) + 'xueweihan')
        return hash.hexdigest()

    def send_mesaage(self):
        # 这里只简单用|分割protocol_id和发送内容
        message = self.protocol_id +'|'+ MESSAGE
        self.socket.sendto(message, (UDP_IP, UDP_PORT))

    def get_message(self):
        data, addr = self.socket.recvfrom(1024)
        if self.server_addr:
            # 客户端也只接收创建链接的服务端的数据包
            if self.server_addr == addr:
                return data
            else:
                return None
        else:
            self.server_addr = addr
            return data

s1 = Udp()
for i in range(2):
    try:
        s1.send_mesaage()
        print s1.get_message()
    except socket.timeout:
        print '链接超时'
        # 清除原来创建链接的数据
        s1.server_addr = None        

s2 = Udp()
for i in range(2):
# 此时是没法创建链接的,由于上一个链接尚未销毁
    try:
        s2.send_mesaage()
        print s2.get_message()
    except socket.timeout:
        print '链接超时'
        # 清除原来创建链接的数据
        s2.server_addr = None

# 暂停2秒,等待服务器注销上一次的链接
time.sleep(2)
s3 = Udp()
for i in range(2):
# 此时是能够创建链接的,由于上面链接以超时
    try:
        s3.send_mesaage()
        print s3.get_message()
    except socket.timeout:
        print '链接超时'
        # 清除原来创建链接的数据
        s3.server_addr = None

上面代码还有不少不足的地方(TODO:超时,部分有问题),因此仅供参考。全部代码都在github上,代码运行效果以下:

相关文章
相关标签/搜索