精华内容
下载资源
问答
  • 网络通讯模型

    千次阅读 2011-03-17 15:55:00
     这和Socket模型非常类似。下面就以老陈接收信件为例讲解Socket IO模型。 <br />一:select模型  老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信,在这种情况下,...

    老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。他们的信会被邮递员投递到他们的信箱里。
      这和Socket模型非常类似。下面就以老陈接收信件为例讲解Socket IO模型。

    一:select模型
      老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信,在这种情况下,“下楼检查信箱”然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。
      select模型和老陈的这种情况非常相似:周而复始地去检查......如果有数据......接收发送.......
      使用线程来select应该是通用的做法:

       1.
       2.
       3. procedure TListenThread.Execute;
       4. var
       5.  addr  TSockAddrIn;
       6.  fd_read  TFDSet;
       7.  timeout  TTimeVal;
       8.  ASock,
       9.  MainSock  TSocket;
      10.  len, i  Integer;
      11. begin
      12.  MainSock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
      13.  addr.sin_family = AF_INET;
      14.  addr.sin_port = htons(5678);
      15.  addr.sin_addr.S_addr = htonl(INADDR_ANY);
      16.  bind( MainSock, @addr, sizeof(addr) );
      17.  listen( MainSock, 5 );
      18.  while (not Terminated) do
      19.  begin
      20.   FD_ZERO( fd_read );
      21.   FD_SET( MainSock, fd_read );
      22.   timeout.tv_sec = 0;
      23.   timeout.tv_usec = 500;
      24.   if select( 0, @fd_read, nil, nil, @timeout )  0 then 至少有1个等待Accept的connection
      25.   begin
      26.    if FD_ISSET( MainSock, fd_read ) then
      27.    begin
      28.    for i=0 to fd_read.fd_count-1 do 注意,fd_count = 64,也就是说select只能同时管理最多64个连接
      29.    begin
      30.     len = sizeof(addr);
      31.     ASock = accept( MainSock, addr, len );
      32.     if ASock  INVALID_SOCKET then
      33.      ....为ASock创建一个新的线程,在新的线程中再不停地select
      34.     end;
      35.    end;   
      36.   end;
      37.  end; while (not self.Terminated)
      38.  shutdown( MainSock, SD_BOTH );
      39.  closesocket( MainSock );
      40. end;


    二:WSAAsyncSelect模型
      后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,一旦信箱里有新的信件,盖茨就会给老陈打电话:喂,大爷,你有新的信件了!从此,老陈再也不必频繁上下楼检查信箱了,牙也不疼了,你瞅准了,蓝天......不是,微软......
      微软提供的WSAAsyncSelect模型就是这个意思。
      WSAAsyncSelect模型是Windows下最简单易用的一种Socket IO模型。使用这种模型时,Windows会把网络事件以消息的形势通知应用程序。
      首先定义一个消息标示常量:

       1.
       2.
       3. const WM_SOCKET = WM_USER + 55;
       4.   再在主Form的private域添加一个处理此消息的函数声明:
       5. private
       6. procedure WMSocket(var Msg TMessage); message WM_SOCKET;
       7.   然后就可以使用WSAAsyncSelect了:
       8. var
       9.  addr  TSockAddr;
      10.  sock  TSocket;
      11.  sock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
      12.  addr.sin_family = AF_INET;
      13.  addr.sin_port = htons(5678);
      14.  addr.sin_addr.S_addr = htonl(INADDR_ANY);
      15.  bind( m_sock, @addr, sizeof(SOCKADDR) );
      16.  WSAAsyncSelect( m_sock, Handle, WM_SOCKET, FD_ACCEPT or FD_CLOSE );
      17.  listen( m_sock, 5 );
      18.  ....
      19.   应用程序可以对收到WM_SOCKET消息进行分析,判断是哪一个socket产生了网络事件以及事件类型:
      20. procedure TfmMain.WMSocket(var Msg TMessage);
      21. var
      22.  sock  TSocket;
      23.  addr  TSockAddrIn;
      24.  addrlen  Integer;
      25.  buf  Array [0..4095] of Char;
      26. begin
      27.  Msg的WParam是产生了网络事件的socket句柄,LParam则包含了事件类型
      28.  case WSAGetSelectEvent( Msg.LParam ) of
      29.  FD_ACCEPT
      30.   begin
      31.    addrlen = sizeof(addr);
      32.    sock = accept( Msg.WParam, addr, addrlen );
      33.    if sock  INVALID_SOCKET then
      34.     WSAAsyncSelect( sock, Handle, WM_SOCKET, FD_READ or FD_WRITE or FD_CLOSE );
      35.   end;
      36.   FD_CLOSE  closesocket( Msg.WParam );
      37.   FD_READ  recv( Msg.WParam, buf[0], 4096, 0 );
      38.   FD_WRITE  ;
      39.  end;
      40. end;


    三:WSAEventSelect模型
       后来,微软的信箱非常畅销,购买微软信箱的人以百万计数......以至于盖茨每天24小时给客户打电话,累得腰酸背痛,喝蚁力神都不好使。微软改进了 他们的信箱:在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出“新信件到达”声,提醒老陈去收信。盖茨终于可以 睡觉了。
      同样要使用线程:

       1.
       2.
       3. procedure TListenThread.Execute;
       4. var
       5.  hEvent  WSAEvent;
       6.  ret  Integer;
       7.  ne  TWSANetworkEvents;
       8.  sock  TSocket;
       9.  adr  TSockAddrIn;
      10.  sMsg  String;
      11.  Index,
      12.  EventTotal  DWORD;
      13.  EventArray  Array [0..WSA_MAXIMUM_WAIT_EVENTS-1] of WSAEVENT;
      14. begin
      15.  ...socket...bind...
      16.  hEvent = WSACreateEvent();
      17.  WSAEventSelect( ListenSock, hEvent, FD_ACCEPT or FD_CLOSE );
      18.  ...listen...
      19.  while ( not Terminated ) do
      20.  begin
      21.   Index = WSAWaitForMultipleEvents( EventTotal, @EventArray[0], FALSE, WSA_INFINITE, FALSE );
      22.   FillChar( ne, sizeof(ne), 0 );
      23.   WSAEnumNetworkEvents( SockArray[Index-WSA_WAIT_EVENT_0], EventArray[Index-WSA_WAIT_EVENT_0], @ne );
      24.   if ( ne.lNetworkEvents and FD_ACCEPT )  0 then
      25.   begin
      26.    if ne.iErrorCode[FD_ACCEPT_BIT]  0 then
      27.     continue;
      28.    ret = sizeof(adr);
      29.    sock = accept( SockArray[Index-WSA_WAIT_EVENT_0], adr, ret );
      30.    if EventTotal  WSA_MAXIMUM_WAIT_EVENTS-1 then这里WSA_MAXIMUM_WAIT_EVENTS同样是64
      31.    begin
      32.     closesocket( sock );
      33.     continue;
      34.    end;
      35.    hEvent = WSACreateEvent();
      36.    WSAEventSelect( sock, hEvent, FD_READ or FD_WRITE or FD_CLOSE );
      37.    SockArray[EventTotal] = sock;
      38.    EventArray[EventTotal] = hEvent;
      39.    Inc( EventTotal );
      40.   end;
      41.   if ( ne.lNetworkEvents and FD_READ )  0 then
      42.   begin
      43.    if ne.iErrorCode[FD_READ_BIT]  0 then
      44.     continue;
      45.     FillChar( RecvBuf[0], PACK_SIZE_RECEIVE, 0 );
      46.     ret = recv( SockArray[Index-WSA_WAIT_EVENT_0], RecvBuf[0], PACK_SIZE_RECEIVE, 0 );
      47.     ......
      48.    end;
      49.   end;
      50. end;
      51.   


    四:Overlapped IO 事件通知模型
       后来,微软通过调查发现,老陈不喜欢上下楼收发信件,因为上下楼其实很浪费时间。于是微软再次改进他们的信箱。新式的信箱采用了更为先进的技术,只要用 户告诉微软自己的家在几楼几号,新式信箱会把信件直接传送到用户的家中,然后告诉用户,你的信件已经放到你的家中了!老陈很高兴,因为他不必再亲自收发信 件了!
      Overlapped IO 事件通知模型和WSAEventSelect模型在实现上非常相似,主要区别在“Overlapped”,Overlapped模型是让应用程序使用重叠 数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock IO请求。这些提交的请求完成后,应用程序会收到通知。什么意思呢?就是说,如果你想从socket上接收数据,只需要告诉系统,由系统为你接收数据,而 你需要做的只是为系统提供一个缓冲区~~~~~
    Listen线程和WSAEventSelect模型一模一样,RecvSend线程则完全不同:

       1.
       2.
       3. procedure TOverlapThread.Execute;
       4. var
       5.  dwTemp  DWORD;
       6.  ret  Integer;
       7.  Index  DWORD;
       8. begin
       9.  ......
      10.  while ( not Terminated ) do
      11.  begin
      12.   Index = WSAWaitForMultipleEvents( FLinks.Count, @FLinks.Events[0], FALSE, RECV_TIME_OUT, FALSE );
      13.   Dec( Index, WSA_WAIT_EVENT_0 );
      14.   if Index  WSA_MAXIMUM_WAIT_EVENTS-1 then 超时或者其他错误
      15.    continue;
      16.   WSAResetEvent( FLinks.Events[Index] );
      17.   WSAGetOverlappedResult( FLinks.Sockets[Index], FLinks.pOverlaps[Index], @dwTemp, FALSE,FLinks.pdwFlags[Index]^ );
      18.   if dwTemp = 0 then 连接已经关闭
      19.   begin
      20.    ......
      21.    continue;
      22.   end else
      23.  begin
      24.   fmMain.ListBox1.Items.Add( FLinks.pBufs[Index]^.buf );
      25.  end;
      26.  初始化缓冲区
      27.  FLinks.pdwFlags[Index]^ = 0;
      28.  FillChar( FLinks.pOverlaps[Index]^, sizeof(WSAOVERLAPPED), 0 );
      29.  FLinks.pOverlaps[Index]^.hEvent = FLinks.Events[Index];
      30.  FillChar( FLinks.pBufs[Index]^.buf^, BUFFER_SIZE, 0 );
      31.  递一个接收数据请求
      32.   WSARecv( FLinks.Sockets[Index], FLinks.pBufs[Index], 1, FLinks.pdwRecvd[Index]^, FLinks.pdwFlags[Index]^, FLinks.pOverlaps[Index], nil );
      33. end;
      34. end;


    五:Overlapped IO 完成例程模型
       老陈接收到新的信件后,一般的程序是:打开信封----掏出信纸----阅读信件----回复信件......为了进一步减轻用户负担,微软又开发了一 种新的技术:用户只要告诉微软对信件的操作步骤,微软信箱将按照这些步骤去处理信件,不再需要用户亲自拆信阅读回复了!老陈终于过上了小资生活!
      Overlapped IO 完成例程要求用户提供一个回调函数,发生新的网络事件的时候系统将执行这个函数:

       1.
       2.
       3. procedure WorkerRoutine( const dwError, cbTransferred  DWORD;
       4. const
       5. lpOverlapped  LPWSAOVERLAPPED; const dwFlags  DWORD ); stdcall;
       6.   然后告诉系统用WorkerRoutine函数处理接收到的数据:
       7. WSARecv( m_socket, @FBuf, 1, dwTemp, dwFlag, @m_overlap, WorkerRoutine );
       8.   然后......没有什么然后了,系统什么都给你做了!微软真实体贴!
       9. while ( not Terminated ) do这就是一个RecvSend线程要做的事情......什么都不用做啊!!!
      10. begin
      11.  if SleepEx( RECV_TIME_OUT, True ) = WAIT_IO_COMPLETION then
      12.  begin
      13.   ;
      14.  end else
      15.  begin
      16.   continue;
      17.  end;
      18. end;
      19.   


    六:IOCP模型
      微软信箱似乎很完美,老陈也很满意。但是在一些大公司情况却完全不同!这些大公司有数以万计的信箱,每秒钟都有数以百计的信件需要处理,以至于微软信箱经常因超负荷运转而崩溃!需要重新启动!微软不得不使出杀手锏......
      微软给每个大公司派了一名名叫“Completion Port”的超级机器人,让这个机器人去处理那些信件!
       “Windows NT小组注意到这些应用程序的性能没有预料的那么高。特别的,处理很多同时的客户请求意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的 [没有被挂起和等待发生什么事],Microsoft意识到NT内核花费了太多的时间来转换运行线程的上下文 [Context],线程就没有得到很多CPU时间来做它们的工作。大家可能也都感觉到并行模型的瓶颈在于它为每一个客户请求都创建了一个新线程。创建线 程比起创建进程开销要小,但也远不是没有开销的。我们不妨设想一下:如果事先开好N个线程,让它们在那hold[堵塞],然后可以将所有用户的请求都投递 到一个消息队列中去。然后那N个线程逐一从消息队列中去取出消息并加以处理。就可以避免针对每一个用户请求都开线程。不仅减少了线程的资源,也提高了线程 的利用率。理论上很不错,你想我等泛泛之辈都能想出来的问题,Microsoft又怎会没有考虑到呢”-----摘自nonocast的《理解IO Completion Port》
      先看一下IOCP模型的实现:

       1.
       2.
       3. 创建一个完成端口
       4. FCompletPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE, 0,0,0 );
       5. 接受远程连接,并把这个连接的socket句柄绑定到刚才创建的IOCP上
       6. AConnect = accept( FListenSock, addr, len);
       7. CreateIoCompletionPort( AConnect, FCompletPort, nil, 0 );
       8. 创建CPU数2 + 2个线程
       9. for i=1 to si.dwNumberOfProcessors2+2 do
      10. begin
      11.  AThread = TRecvSendThread.Create( false );
      12.  AThread.CompletPort = FCompletPort;告诉这个线程,你要去这个IOCP去访问数据
      13. end;
      14.   就这么简单,我们要做的就是建立一个IOCP,把远程连接的socket句柄绑定到刚才创建的IOCP上,最后创建n个线程,并告诉这n个线程到这个IOCP上去访问数据就可以了。
      15.   再看一下TRecvSendThread线程都干些什么:
      16. procedure TRecvSendThread.Execute;
      17. var
      18.  ......
      19. begin
      20.  while (not self.Terminated) do
      21.  begin
      22.   查询IOCP状态(数据读写操作是否完成)
      23.   GetQueuedCompletionStatus( CompletPort, BytesTransd, CompletKey, POVERLAPPED(pPerIoDat), TIME_OUT );
      24.   if BytesTransd  0 then
      25.    ....;数据读写操作完成
      26.   
      27.    再投递一个读数据请求
      28.    WSARecv( CompletKey, @(pPerIoDat^.BufData), 1, BytesRecv, Flags, @(pPerIoDat^.Overlap), nil );
      29.   end;
      30. end;
      31.   


    读写线程只是简单地检查IOCP是否完成了我们投递的读写操作,如果完成了则再投递一个新的读写请求。
      应该注意到,我们创建的所有TRecvSendThread都在访问同一个IOCP(因为我们只创建了一个IOCP),并且我们没有使用临界区!难道不会产生冲突吗?不用考虑同步问题吗?
      这正是IOCP的奥妙所在。IOCP不是一个普通的对象,不需要考虑线程安全问题。它会自动调配访问它的线程:如果某个socket上有一个线程A正在访问,那么线程B的访问请求会被分配到另外一个socket。这一切都是由系统自动调配的,我们无需过问。

    展开全文
  • linux网络通讯模型

    千次阅读 2017-05-23 11:50:28
    一、网络通信 网络是通过物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的,通过信息交换实现人与人、人与计算机、计算机与计算机之间的通信。 1. 网络通信要遵守网络...

    一、网络通信

    网络是通过物理链路将各个孤立的工作站或主机相连在一起,组成数据链路,从而达到资源共享和通信的目的,通过信息交换实现人与人、人与计算机、计算机与计算机之间的通信。

    1. 网络通信要遵守网络协议,局域网中最常用的有三个网络协议:MICROSOFT的NETBEUI、NOVELL的IPX/SPX和TCP/IP协议。

    a> NetBEUI - 网络基本输入输出系统扩展用户接口。NETBEUI是为IBM开发的非路由协议,用于携带NETBIOS通信。NETBEUI缺乏路由和网络层寻址功能,既是其最大的优点,也是其最大的缺点。

    b> IPX/SPX - 互连网包交换/顺序包交换。IPX是NOVELL用于NETWARE客户端/服务器的协议群组,具有完全的路由能力,但是可扩展性受到其高层广播通信和高开销的限制。

    c> TCP/IP允许与Internet完全的连接,这是其它协议不具备的。同时具备了可扩展性和可靠性的需求,但是不牺牲了速度和效率。

    二、TCP/IP协议

    1. 参考模型

    国际标准化组织(ISO)定义了网络协议的基本框架,被称为OSI模型。OSI模型包括应用层、表示层、会话层、传输层、网络层、数据链路层及物理层。这个7层的协议模型虽然规定得非常细致和完善,但在实际中却得不到广泛的应用,其重要的原因之一就在于它过于复杂,但它仍是此后很多协议模型的基础。与此相区别的TCP/IP协议模型将OSI的7层协议模型简化为4层,从而更有利于实现和使用。TCP/IP协议模型包括应用层、传输层、网络层、网络接口层。


    2. TCP/IP协议的4层协议

    TCP/IP协议是网络中使用的基本通信协议。

    网络接口层:网络接口层是TCP/IP的最底层,负责将二进制流转换成数据帧,并进行数据帧的发送和接收。数据帧是网络传输的基本单元。
    网络层:网络层负责在主机之间的通信中选择数据包的传输路径,即 路由。当网络收到传输层的请求后,使用路由算法来确定是直接交付数据包,还是把它传递给路由器,最后把数据包交给适当的网络接口进行传输。
    传输层:负责实现应用程序之间的通信服务,又称为端到端通信,传输层要提供可靠的传输服务,以确保数据到达无差错、无乱序。为了达到这个目的,传输层协议软件要进行协商。传输层协议软件要把传输的数据流分为分组。
    应用层:应用层是分层模型的最高层。应用程序使用相应的应用层协议,把封装好的数据交给传输层或是传输层接收数据并处理。

    3. 分层模型图


    4. TCP与UDP的概念

    TCP(传输控制协议):为应用程序提供可靠的通信连接,适合一次传输大批数据的情况,并适应要求得到相应的应用程序。

    UDP(用户数据包协议):提供无连接通信,且不对传送包进行可靠的保证,适合一次传输少量数据。

    三、TCP协议

    TCP是TCP/IP体系中面向连接的传输层协议,提供全双工和可靠交付的服务,采用许多机制来确保数据的可靠性。

    首先,TCP为发送的数据加上序列号,来保证数据能被接收方接收,且只能被正确接收一次。其次,TCP采用具有重传功能的确认技术作为可靠数据流传输服务的基础。由于发送方接收到数据之后会返回一个ACK信号,所以发送方检测ACK信号来判断是否接收成功。如果接收失败,就会重新发送刚才的数据。最后,TCP采用可变长的滑动窗口协议进行流量控制,防止发送方与接收方数据不匹配造成数据丢失。采用可变长的滑动窗口,可以对接收/发送的数据进行动态调节,灵活性更强。

    四、UDP协议

    UDP也是TCP/IP体系中面向连接的传输层协议,是一种无连接协议,因此不需要三次握手来建立连接。UDP协议比TCP协议更为高效,解决了实时性的问题。

    展开全文
  • 127.0.0.1 8888
  • 网络游戏通讯模型初探

    千次阅读 2004-12-28 20:40:00
    序言 网络游戏,作为游戏与网络有机结合的产物,把玩家带入了新的娱乐领域。网络游戏在中国开始发展至今也仅有3,4年的历史,跟已经拥有几十年开发历史的单机游戏相比,网络游戏还是非常年轻的。当然,它的形成也是...

    序言

      网络游戏,作为游戏与网络有机结合的产物,把玩家带入了新的娱乐领域。网络游戏在中国开始发展至今也仅有3,4年的历史,跟已经拥有几十年开发历史的单机游戏相比,网络游戏还是非常年轻的。当然,它的形成也是根据历史变化而产生的可以说没有互联网的兴起,也就没有网络游戏的诞生。作为新兴产物,网络游戏的开发对广大开发者来说更加神秘,对于一个未知领域,开发者可能更需要了解的是网络游戏与普通单机游戏有何区别,网络游戏如何将玩家们连接起来,以及如何为玩家提供一个互动的娱乐环境。本文就将围绕这三个主题来给大家讲述一下网络游戏的网络互连实现方法。
     网络游戏与单机游戏

      说到网络游戏,不得不让人联想到单机游戏,实际上网络游戏的实质脱离不了单机游戏的制作思想,网络游戏和单机游戏的差别大家可以很直接的想到:不就是可以多人连线吗?没错,但如何实现这些功能,如何把网络连线合理的融合进单机游戏,就是我们下面要讨论的内容。在了解网络互连具体实现之前,我们先来了解一下单机与网络游戏它们各自的运行流程,只有了解这些,你才能深入网络游戏开发的核心。


    现在先让我们来看一下普通单机游戏的简化执行流程:

    Initialize() // 初始化模块
    {
     初始化游戏数据;
    }
    Game() // 游戏循环部分
    {
     绘制游戏场景、人物以及其它元素;
     获取用户操作输入;
     switch( 用户输入数据)
     {
      case 移动:
      {
       处理人物移动;
      }
      break;
      case 攻击:
      {
       处理攻击逻辑:
      }
      break;
      ...
      其它处理响应;
      ...
      default:
       break;
     }
     游戏的NPC等逻辑AI处理;
    }
    Exit() // 游戏结束
    {
     释放游戏数据;
     离开游戏;
    }


      我们来说明一下上面单机游戏的流程。首先,不管是游戏软件还是其他应用软件,初始化部分必不可少,这里需要对游戏的数据进行初始化,包括图像、声音以及一些必备的数据。接下来,我们的游戏对场景、人物以及其他元素进行循环绘制,把游戏世界展现给玩家,同时接收玩家的输入操作,并根据操作来做出响应,此外,游戏还需要对NPC以及一些逻辑AI进行处理。最后,游戏数据被释放,游戏结束。
      网络游戏与单机游戏有一个很显著的差别,就是网络游戏除了一个供操作游戏的用户界面平台(如单机游戏)外,还需要一个用于连接所有用户,并为所有用户提供数据服务的服务器,从某些角度来看,游戏服务器就像一个大型的数据库,提供数据以及数据逻辑交互的功能。让我们来看看一个简单的网络游戏模型执行流程:

     

     客户机:

    Login()// 登入模块
    {
     初始化游戏数据;
     获取用户输入的用户和密码;
     与服务器创建网络连接;
     发送至服务器进行用户验证;
     ...
     等待服务器确认消息;
     ...
     获得服务器反馈的登入消息;
     if( 成立 )
      进入游戏;
     else
      提示用户登入错误并重新接受用户登入;
    }
    Game()// 游戏循环部分
    {
     绘制游戏场景、人物以及其它元素;
     获取用户操作输入;
     将用户的操作发送至服务器;
     ...
     等待服务器的消息;
     ...
     接收服务器的反馈信息;
     switch( 服务器反馈的消息数据 )
     {
      case 本地玩家移动的消息:
      {
       if( 允许本地玩家移动 )
        客户机处理人物移动;
       else
        客户机保持原有状态;
      }
       break;
      case 其他玩家/NPC的移动消息:
      {
       根据服务器的反馈信息进行其他玩家或者NPC的移动处理;
      }
      break;
      case 新玩家加入游戏:
      {
       在客户机中添加显示此玩家;
      }
       break;
      case 玩家离开游戏:
      {
       在客户机中销毁此玩家数据;
      }
       break;
      ...
      其它消息类型处理;
      ... 
      default:
       break;
     }
    }
    Exit()// 游戏结束
    {
     发送离开消息给服务器;
     ...
     等待服务器确认;
     ...
     得到服务器确认消息;
     与服务器断开连接;
     释放游戏数据;
     离开游戏;
    }


      服务器:

    Listen()  // 游戏服务器等待玩家连接模块
    {
     ...
     等待用户的登入信息;
     ...
     接收到用户登入信息;
     分析用户名和密码是否符合;
     if( 符合 )
     {
      发送确认允许进入游戏消息给客户机; 
      把此玩家进入游戏的消息发布给场景中所有玩家;
      把此玩家添加到服务器场景中;
     }
     else
     {
      断开与客户机的连接;
     }
    }
    Game() // 游戏服务器循环部分
    {
     ...
     等待场景中玩家的操作输入;
     ...
     接收到某玩家的移动输入或NPC的移动逻辑输入;
     // 此处只以移动为例
     进行此玩家/NPC在地图场景是否可移动的逻辑判断;

     if( 可移动 )
     {
      对此玩家/NPC进行服务器移动处理;
      发送移动消息给客户机;
      发送此玩家的移动消息给场景上所有玩家;
     }
     else
      发送不可移动消息给客户机;
    }
    Exit()  // 游戏服务=器结束
    {
     接收到玩家离开消息;
     将此消息发送给场景中所有玩家;
     发送允许离开的信息;
     将玩家数据存入数据库;
     注销此玩家在服务器内存中的数据;
    }
    }


      让我们来说明一下上面简单网络游戏模型的运行机制。先来讲讲服务器端,这里服务器端分为三个部分(实际上一个完整的网络游戏远不止这些):登入模块、游戏模块和登出模块。登入模块用于监听网络游戏客户端发送过来的网络连接消息,并且验证其合法性,然后在服务器中创建这个玩家并且把玩家带领到游戏模块中; 游戏模块则提供给玩家用户实际的应用服务,我们在后面会详细介绍这个部分; 在得到玩家要离开游戏的消息后,登出模块则会把玩家从服务器中删除,并且把玩家的属性数据保存到服务器数据库中,如: 经验值、等级、生命值等。

      接下来让我们看看网络游戏的客户端。这时候,客户端不再像单机游戏一样,初始化数据后直接进入游戏,而是在与服务器创建连接,并且获得许可的前提下才进入游戏。除此之外,网络游戏的客户端游戏进程需要不断与服务器进行通讯,通过与服务器交换数据来确定当前游戏的状态,例如其他玩家的位置变化、物品掉落情况。同样,在离开游戏时,客户端会向服务器告知此玩家用户离开,以便于服务器做出相应处理。


    以上用简单的伪代码给大家阐述了单机游戏与网络游戏的执行流程,大家应该可以清楚看出两者的差别,以及两者间相互的关系。我们可以换个角度考虑,网络游戏就是把单机游戏的逻辑运算部分搬移到游戏服务器中进行处理,然后把处理结果(包括其他玩家数据)通过游戏服务器返回给连接的玩家。
      网络互连

      在了解了网络游戏基本形态之后,让我们进入真正的实际应用部分。首先,作为网络游戏,除了常规的单机游戏所必需的东西之外,我们还需要增加一个网络通讯模块,当然,这也是网络游戏较为主要的部分,我们来讨论一下如何实现网络的通讯模块。

      一个完善的网络通讯模块涉及面相当广,本文仅对较为基本的处理方式进行讨论。网络游戏是由客户端和服务器组成,相应也需要两种不同的网络通讯处理方式,不过也有相同之处,我们先就它们的共同点来进行介绍。我们这里以Microsoft Windows 2000 [2000 Server]作为开发平台,并且使用Winsock作为网络接口(可能一些朋友会考虑使用DirectPlay来进行网络通讯,不过对于当前在线游戏,DirectPlay并不适合,具体原因这里就不做讨论了)。

     

      确定好平台与接口后,我们开始进行网络连接创建之前的一些必要的初始化工作,这部分无论是客户端或者服务器都需要进行。让我们看看下面的代码片段:

    WORD wVersionRequested;
    WSADATAwsaData;
    wVersionRequested MAKEWORD(1, 1);
    if( WSAStartup( wVersionRequested, &wsaData ) !0 )
    {
     Failed( WinSock Version Error! );
    }


      上面通过调用Windows的socket API函数来初始化网络设备,接下来进行网络Socket的创建,代码片段如下:

    SOCKET sSocket socket( AF_INET, m_lProtocol, 0 );
    if( sSocket == INVALID_SOCKET )
    {
     Failed( WinSocket Create Error! );
    }


      这里需要说明,客户端和服务端所需要的Socket连接数量是不同的,客户端只需要一个Socket连接足以满足游戏的需要,而服务端必须为每个玩家用户创建一个用于通讯的Socket连接。当然,并不是说如果服务器上没有玩家那就不需要创建Socket连接,服务器端在启动之时会生成一个特殊的Socket用来对玩家创建与服务器连接的请求进行响应,等介绍网络监听部分后会有更详细说明。

     

      有初始化与创建必然就有释放与删除,让我们看看下面的释放部分:

    if( sSocket != INVALID_SOCKET )
    {
     closesocket( sSocket );
    }
    if( WSACleanup() != 0 )
    {
     Warning( Cant release Winsocket );
    }

     


      这里两个步骤分别对前面所作的创建初始化进行了相应释放。

      接下来看看服务器端的一个网络执行处理,这里我们假设服务器端已经创建好一个Socket供使用,我们要做的就是让这个Socket变成监听网络连接请求的专用接口,看看下面代码片段:

    SOCKADDR_IN addr;
    memset( &addr, 0, sizeof(addr) );
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl( INADDR_ANY );
    addr.sin_port = htons( Port );  // Port为要监听的端口号
    // 绑定socket
    if( bind( sSocket, (SOCKADDR*)&addr, sizeof(addr) ) == SOCKET_ERROR )
    {
     Failed( WinSocket Bind Error!);
    }
    // 进行监听
    if( listen( sSocket, SOMAXCONN ) == SOCKET_ERROR )
    {
     Failed( WinSocket Listen Error!);
    }


      这里使用的是阻塞式通讯处理,此时程序将处于等待玩家用户连接的状态,倘若这时候有客户端连接进来,则通过accept()来创建针对此玩家用户的Socket连接,代码片段如下:

    sockaddraddrServer;
    int nLen sizeof( addrServer );
    SOCKET sPlayerSocket accept( sSocket, &addrServer, &nLen );
    if( sPlayerSocket == INVALID_SOCKET )
    {
     Failed( WinSocket Accept Error!);
    }


      这里我们创建了sPlayerSocket连接,此后游戏服务器与这个玩家用户的通讯全部通过此Socket进行,到这里为止,我们服务器已经有了接受玩家用户连接的功能,现在让我们来看看游戏客户端是如何连接到游戏服务器上,代码片段如下:

    SOCKADDR_IN addr;
    memset( &addr, 0, sizeof(addr) );
    addr.sin_family = AF_INET;// 要连接的游戏服务器端口号
    addr.sin_addr.s_addr = inet_addr( IP );// 要连接的游戏服务器IP地址,
    addr.sin_port = htons( Port );//到此,客户端和服务器已经有了通讯的桥梁,
    //接下来就是进行数据的发送和接收:
    connect( sSocket, (SOCKADDR*)&addr, sizeof(addr) );
    if( send( sSocket, pBuffer, lLength, 0 ) == SOCKET_ERROR )
    {
     Failed( WinSocket Send Error!);
    }


      这里的pBuffer为要发送的数据缓冲指针,lLength为需要发送的数据长度,通过这支Socket API函数,我们无论在客户端或者服务端都可以进行数据的发送工作,同时,我们可以通过recv()这支Socket API函数来进行数据接收:

    if( recv( sSocket, pBuffer, lLength, 0 ) == SOCKET_ERROR )
    {
     Failed( WinSocket Recv Error!);
    }


      其中pBuffer用来存储获取的网络数据缓冲,lLength则为需要获取的数据长度。

      现在,我们已经了解了一些网络互连的基本知识,但作为网络游戏,如此简单的连接方式是无法满足网络游戏中百人千人同时在线的,我们需要更合理容错性更强的网络通讯处理方式,当然,我们需要先了解一下网络游戏对网络通讯的需求是怎样的。
    大家知道,游戏需要不断循环处理游戏中的逻辑并进行游戏世界的绘制,上面所介绍的Winsock处理方式均是以阻塞方式进行,这样就违背了游戏的执行本质,可以想象,在客户端连接到服务器的过程中,你的游戏不能得到控制,这时如果玩家想取消连接或者做其他处理,甚至显示一个最基本的动态连接提示都不行。

      所以我们需要用其他方式来处理网络通讯,使其不会与游戏主线相冲突,可能大家都会想到: 创建一个网络线程来处理不就可以了?没错,我们可以创建一个专门用于网络通讯的子线程来解决这个问题。当然,我们游戏中多了一个线程,我们就需要做更多的考虑,让我们来看看如何创建网络通讯线程。

      在Windows系统中,我们可以通过CreateThread()函数来进行线程的创建,看看下面的代码片段:

    DWORD dwThreadID;
    HANDLE hThread = CreateThread( NULL, 0, NetThread/*网络线程函式*/, sSocket, 0, &dwThreadID );
    if( hThread == NULL )
    {
     Failed( WinSocket Thread Create Error!);
    }


      这里我们创建了一个线程,同时将我们的Socket传入线程函数:

    DWORD WINAPINetThread(LPVOID lParam)


    {
     SOCKET sSocket (SOCKET)lParam;
     ...
     return 0;
    }

     

      NetThread就是我们将来用于处理网络通讯的网络线程。那么,我们又如何把Socket的处理引入线程中?

      看看下面的代码片段:

    HANDLE hEvent;
    hEvent = CreateEvent(NULL,0,0,0);
    // 设置异步通讯
    if( WSAEventSelect( sSocket, hEvent,
    FD_ACCEPT|FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE ) ==SOCKET_ERROR )
    {
     Failed( WinSocket EventSelect Error!);
    }


      通过上面的设置之后,WinSock API函数均会以非阻塞方式运行,也就是函数执行后会立即返回,这时网络通讯会以事件方式存储于hEvent,而不会停顿整支程式。

      完成了上面的步骤之后,我们需要对事件进行响应与处理,让我们看看如何在网络线程中获得网络通讯所产生的事件消息:

    WSAEnumNetworkEvents( sSocket, hEvent, &SocketEvents );
    if( SocketEvents.lNetworkEvents != 0 )
    {
     switch( SocketEvents.lNetworkEvents )
     {
      case FD_ACCEPT:
       WSANETWORKEVENTS SocketEvents;
       break;
      case FD_CONNECT:
      {
       if( SocketEvents.iErrorCode[FD_CONNECT_BIT] == 0)
       // 连接成功  
       {
       // 连接成功后通知主线程(游戏线程)进行处理
       }
      }
       break;
      case FD_READ:
      // 获取网络数据
      {
       if( recv( sSocket, pBuffer, lLength, 0) == SOCKET_ERROR )
       {
        Failed( WinSocket Recv Error!);
       }
      }
       break;
      case FD_WRITE:
       break;
      case FD_CLOSE:
       // 通知主线程(游戏线程), 网络已经断开
       break;
      default:
       break;
     }


      这里仅对网络连接(FD_CONNECT) 和读取数据(FD_READ) 进行了简单模拟操作,但实际中网络线程接收到事件消息后,会对数据进行组织整理,然后再将数据回传给我们的游戏主线程使用,游戏主线程再将处理过的数据发送出去,这样一个往返就构成了我们网络游戏中的数据通讯,是让网络游戏动起来的最基本要素。

      最后,我们来谈谈关于网络数据包(数据封包)的组织,网络游戏的数据包是游戏数据通讯的最基本单位,网络游戏一般不会用字节流的方式来进行数据传输,一个数据封包也可以看作是一条消息指令,在游戏进行中,服务器和客户端会不停的发送和接收这些消息包,然后将消息包解析转换为真正所要表达的指令意义并执行。
     互动与管理

      说到互动,对于玩家来说是与其他玩家的交流,但对于计算机而言,实现互动也就是实现数据消息的相互传递。前面我们已经了解过网络通讯的基本概念,它构成了互动的最基本条件,接下来我们需要在这个网络层面上进行数据的通讯。遗憾的是,计算机并不懂得如何表达玩家之间的交流,因此我们需要提供一套可让计算机了解的指令组织和解析机制,也就是对我们上面简单提到的网络数据包(数据封包)的处理机制。


    为了能够更简单的给大家阐述网络数据包的组织形式,我们以一个聊天处理模块来进行讨论,看看下面的代码结构:

    struct tagMessage{
     long lType;
     long lPlayerID;
    };
    // 消息指令
    // 指令相关的玩家标识
    char strTalk[256]; // 消息内容


      上面是抽象出来的一个极为简单的消息包结构,我们先来谈谈其各个数据域的用途:

      首先,lType 是消息指令的类型,这是最为基本的消息标识,这个标识用来告诉服务器或客户端这条指令的具体用途,以便于服务器或客户端做出相应处理。lPlayerID 被作为玩家的标识。大家知道,一个玩家在机器内部实际上也就是一堆数据,特别是在游戏服务器中,可能有成千上万个玩家,这时候我们需要一个标记来区分玩家,这样就可以迅速找到特定玩家,并将通讯数据应用于其上。

      strTalk 是我们要传递的聊天数据,这部分才是真正的数据实体,前面的参数只是数据实体应用范围的限定。

      在组织完数据之后,紧接着就是把这个结构体数据通过Socket 连接发送出去和接收进来。这里我们要了解,网络在进行数据传输过程中,它并不关心数据采用的数据结构,这就需要我们把数据结构转换为二进制数据码进行发送,在接收方,我们再将这些二进制数据码转换回程序使用的相应数据结构。让我们来看看如何实现:

    tagMessageMsg;
    Msg.lTypeMSG_CHAT;
    Msg.lPlayerID 1000;
    strcpy( &Msg.strTalk, 聊天信息 ); 


      首先,我们假设已经组织好一个数据包,这里MSG_CHAT 是我们自行定义的标识符,当然,这个标识符在服务器和客户端要统一。玩家的ID 则根据游戏需要来进行设置,这里1000 只作为假设,现在继续:

    char* p = (char*)&Msg;
    long lLength = sizeof( tagMessage );
    send( sSocket, p, lLength );
    // 获取数据结构的长度


      我们通过强行转换把结构体转变为char 类型的数据指针,这样就可以通过这个指针来进行流式数据处理,这里通过sizeof() 获得结构体长度,然后用WinSock 的Send() 函数将数据发送出去。

      接下来看看如何接收数据:

    long lLength = sizeof( tagMessage );
    char* Buffer = new char[lLength];
    recv( sSocket, Buffer, lLength );
    tagMessage* p = (tagMessage*)Buffer;
    // 获取数据


      在通过WinSock 的recv() 函数获取网络数据之后,我们同样通过强行转换把获取出来的缓冲数据转换为相应结构体,这样就可以方便地对数据进行访问。(注:强行转换仅仅作为数据转换的一种手段,实际应用中有更多可选方式,这里只为简洁地说明逻辑)谈到此处,不得不提到服务器/ 客户端如何去筛选处理各种消息以及如何对通讯数据包进行管理。无论是服务器还是客户端,在收到网络消息的时候,通过上面的数据解析之后,还必须对消息类型进行一次筛选和派分,简单来说就是类似Windows 的消息循环,不同消息进行不同处理。这可以通过一个switch 语句(熟悉Windows 消息循环的朋友相信已经明白此意),基于消
    息封包里的lType 信息,对消息进行区分处理,考虑如下代码片段:

    switch( p->lType ) // 这里的p->lType为我们解析出来的消息类型标识
    {
     case MSG_CHAT: // 聊天消息
      break;
     case MSG_MOVE: // 玩家移动消息
      break;
     case MSG_EXIT: // 玩家离开消息
      break;
     default:
      break;
    }


      上面片段中的MSG_MOVE 和MSG_EXIT 都是我们虚拟的消息标识(一个真实游戏中的标识可能会有上百个,这就需要考虑优化和优先消息处理问题)。此外,一个网络游戏服务器面对的是成百上千的连接用户,我们还需要一些合理的数据组织管理方式来进行相关处理。普通的单体游戏服务器,可能会因为当机或者用户过多而导致整个游戏网络瘫痪,而这也就引入分组服务器机制,我们把服务器分开进行数据的分布式处理。

      我们把每个模块提取出来,做成专用的服务器系统,然后建立一个连接所有服务器的数据中心来进行数据交互,这里每个模块均与数据中心创建了连接,保证了每个模块的相关性,同时玩家转变为与当前提供服务的服务器进行连接通讯,这样就可以缓解单独一台服务器所承受的负担,把压力分散到多台服务器上,同时保证了数据的统一,而且就算某台服务器因为异常而当机也不会影响其他模块的游戏玩家,从而提高了整体稳定性。

      分组式服务器缓解了服务器的压力,但也带来了服务器调度问题,分组式服务器需要对服务器跳转进行处理,就以一个玩家进行游戏场景跳转作为讨论基础:假设有一玩家处于游戏场景A,他想从场景A 跳转到场景B,在游戏中,我们称之场景切换,这时玩家就会触发跳转需求,比如走到了场景中的切换点,这样服务器就把玩家数据从游戏场景A 服务器删除,同时在游戏场景B 服务器中把玩家建立起来。

      这里描述了场景切换的简单模型,当中处理还有很多步骤,不过通过这样的思考相信大家可以派生出很多应用技巧。不过需要注意的是,在场景切换或者说模块间切换的时候,需要切实考虑好数据的传输安全以及逻辑合理性,否则切换很可能会成为将来玩家复制物品的桥梁。

     

      总结

      本篇讲述的都是通过一些简单的过程来进行网络游戏通讯,提供了一个制作的思路,虽然具体实现起来还有许多要做,但只要顺着这个思路去扩展、去完善,相信大家很快就能够编写出自己的网络通讯模块。由于时间仓促,本文在很多细节方面都有省略,文中若有错误之处也望大家见谅。


     

    展开全文
  • 从本节开始将开始深入学习Dubbo网络通讯的底层实现细节,在深入学习Dubbo网络模型时,首先应从整体上了解Dubbo的网络通讯模型、线程模型是怎样的?下图是Dubbo官方给出的线程模型: 涉及如下方面: 1)网络...

       从本节开始将开始深入学习Dubbo网络通讯的底层实现细节,在深入学习Dubbo网络模型时,首先应从整体上了解Dubbo的网络通讯模型、线程模型是怎样的?下图是Dubbo官方给出的线程模型:
    这里写图片描述
       涉及如下方面:

    • 网络调用客户端。
    • 网络调用服务端。
    • 网络传输,编解码、序列化。
    • 网络服务端转发模型、线程池。

       下面给出与上述网络模型对应的详细类图:
    这里写图片描述
       上述类做一个简单的结束,后续篇章将会一一详细分析。

    1. 基础接口
    • Resetable 可重置。
    • Endpoint 端(服务端、客户端基接口)
    1. 服务端
    • Server 服务端根接口
    • ExchangeServer 服务端交换机,默认实现Server,内部持有具体Server的实现。
    • HeaderExchangeServer 基于协议头的服务端交互机。
    1. 客户端
    • Channel 客户端通道描述接口。
    • Client 客户端基础接口,继承自Endpoint,Channel,主要定义重连接口。
    1. 传输层
    • Transporter 定义根据URL创建服务端或客户端,内部实现就是构建Server,Client对象。
    1. 编解码
    • Codec2 定义编解码对应的接口。
         下面以Dubbo协议为例,底层网络通信组建基于Netty,Dubbo协议创建服务端的流程如下所示:
      这里写图片描述
         下面还是以Dubbo协议为例,底层网络通信组件基于Netty,Dubbo协议消费端(客户端)建立网络流程图如下:
      这里写图片描述
         上述这些流程图将会在后文的服务端、客户端启动流程时重点分析。

    欢迎加笔者微信号(dingwpmz),加群探讨,笔者优质专栏目录:
    1、源码分析RocketMQ专栏(40篇+)
    2、源码分析Sentinel专栏(12篇+)
    3、源码分析Dubbo专栏(28篇+)
    4、源码分析Mybatis专栏
    5、源码分析Netty专栏(18篇+)
    6、源码分析JUC专栏
    7、源码分析Elasticjob专栏
    8、Elasticsearch专栏(20篇+)
    9、源码分析MyCat专栏

    展开全文
  • 点对点的即时通讯模型

    千次阅读 2011-01-31 23:00:00
    点对点的即时通讯模型
  • 文章目录通讯协议TCP/IP协议TCP/IP模型osi版基本版应用层传输层网络层数据链层 通讯协议 我们想要进⾏数据通讯分⼏步? 1、找到对⽅ip 2 、数据要发送到对⽅指定的应⽤程序上。为了标识这些应⽤程序,所以给这些 ⽹络...
  •  答:无线网络通讯协议有:WAP、WIFI,又称802.11b标准、802.15标准、805.16标准、802.20标准、红外线、蓝牙、GPRS、CDMA1X协议、W-CDMA、CDMA2000和TD-SCDMA三个主流3G标准、WEP协议。 如:1. W-CDMA:即...
  • ROS 通讯模型

    千次阅读 2015-09-22 09:02:47
    ROS 通讯模型  术语定义: Name: 图模型中的Name 在ROS的封装体系中非常重要,所有的resource(从node到topic到service和parameter等)都是在某个namespace中用特定的Name进行了定义。 ...
  • 网络通信模型

    千次阅读 2019-06-02 23:31:00
    网络通信模型 一,起源 由于计算机网络的飞速发展,各大产商根据自己的协议生产不同的硬件和原件,为了实现不同的网络之间的互相通信,iso和ieee相继提出了osi参考模型,和tcp、ip模型 二,osi...
  • 网络分层模型简介

    千次阅读 2018-08-03 14:42:39
    自上而下:应用层,表示层,会话层,传输层,网络层,链路层,物理层 物理层:最底层或第一层,该层包括物理联网媒介,双绞线、同轴电缆、光纤等。物理层的协议产生并检测电压以便发送和接收携带数据的信号,一般...
  • 网络编程模型网络编程三要素

    千次阅读 2016-05-08 19:13:50
    网络编程模型网络编程三要素
  • 网络七层模型

    万次阅读 多人点赞 2018-07-24 22:26:12
    最近又看到这个七层模型了,一直都记不住这个七层模型,就算背住了也很快忘记。主要原因还是因为没有真实的使用场景,也没能理解其中的原理。但是这个东西是计算机网络的基础,既然碰巧看到就顺便整理一下吧。很多...
  • 目录 一、简介 二、相关概念 三、名词解释 ...四、网络参考模型 4.1、OSI参考模型 4.2、TCP/IP模型 4.3、OSI与TCP/IP模型比较 【扩展资料】 一、简介 互联网协议(Internet Protocol S...
  •  Steed是本人在公司从事通信引擎开发多年的经验基础上开发的一套通用的网络通信库,使用C++语言开发,支持高并发连接,底层使用IOCP技术。设计模式参考了著名的C++准标准库Boost中的Asio库,Steed网络通信库发布有...
  • OSI七层模型详解

    万次阅读 多人点赞 2011-12-28 19:43:16
    OSI 七层模型通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯,因此其最主要的功能就是帮助不同类型的主机实现数据传输 。 完成中继功能的节点通常称为中继系统。在OSI七层模型中,处于不同层的...
  • JAVA之TCP网络通讯

    千次阅读 2016-12-12 16:49:32
    TCP(Transmission Control Protocol 传输控制协议)是一种面向...基于TCP网络通讯实现的类主要有服务器端的ServerSocket用客户端的Socket。通讯流程: 打开服务器,等待客户端连接-->客户端连接上服务器-->数据通讯
  • OSI七层网络模型

    千次阅读 2016-09-02 12:27:12
    概述:OSI是一个开放性的通信系统互连参考模型,他是一个定义得非常好的协议规范。OSI模型有7层结构,每层都可以有几个子层。 OSI的7层从上到下分别是 7 应用层 6 表示层...应用层与其它计算机进行通讯的一个应用,它是
  • 计算机七层网络模型

    千次阅读 2018-06-06 11:07:25
    但是这个东西是计算机网络的基础,既然碰巧看到就顺便整理一下吧。很多知识的梳理都是通过文章来理解贯通的,所以在计算机开发中对于技术的应用对敲代码;对于抽象的知识多写文章,自然而然的就懂了。  关于七层...
  • 嵌入式网络通讯详解

    千次阅读 2017-02-24 17:45:26
    前言 最近在做网络相关的项目,然而大学学的网络编程以及网络协议相关的知识都已经忘得差不多了。庆幸的是网上牛人多,百度一下发现...我下定决心要把网络的相关知识从新梳理一遍,因而记录如下。ISO参考模型 ISO/OS

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 111,473
精华内容 44,589
关键字:

网络通讯模型