Project1

标题: Socket网络编程【附RMVA工程】 [打印本页]

作者: 逸豫    时间: 2014-2-15 00:06
标题: Socket网络编程【附RMVA工程】
本帖最后由 逸豫 于 2014-2-15 00:16 编辑

啊,没错我知道的今天是情人节与元宵节,在这种与我无关的日子里还在干这种活真是对不起了

纯Ruby实现的Socket网络通信
稍微封装了一下,采用了非阻塞的方式,注意每帧调用update

使用方法

RUBY 代码复制
  1. #使用前请先调用
  2. NET.init
  3. #创建Socket类
  4. socket = NET::Socket.new
  5. #连接
  6. socket.connect("127.0.0.1",20001)
  7. #每帧刷新
  8. loop do
  9.   Graphics.update
  10.   Input.update
  11.   socket.update
  12.   #接收到消息
  13.   if socket.recv
  14.     socket.send(socket.recv)
  15.   end
  16. end
  17.  
  18. #另附
  19. #在port端口上进行监听
  20. socket.listen(port)
  21. #获取监听得到的客户端socket
  22. socket.accepted_sockets.each do |cs|
  23.   #可以直接操纵
  24.   cs.send("welcome")
  25. end
  26. #是否已经连接,因为使用了非阻塞的方法,从调用connect方法到实际连接上有延迟
  27. socket.connected?
  28. #是否有错误
  29. socket.error?
  30. #错误码可以通过以下代码获取,该错误码与WSAGetLastError获取的错误码一致
  31. socket.errcode
  32. #是否有内容需要发送
  33. socket.sending?
  34. #是否在监听状态
  35. socket.listening?
  36. #停止监听(仅仅是不调用accept方法而已)
  37. socket.stop_listening
  38. #继续监听
  39. socket.continue_listening


以上脚本仅供参考,并未经测试

一些吐槽
用Ruby操作Win32API就好像在一堆void指针上跳舞
一开始看错了结构体定义导致了一个需要400字节长度的结构体指针的参数被我直接传送了一个14字节长的字符串……而且在win7下竟然表现良好,在WINXP下也总是在奇怪的地方报内存错误,在没调试器的情况下DEBUG真是恶心(其实有也是一样恶心就是了……
没使用select,因为select还要自己设计那啥结构,我懒……
在一条消息还没发出去之前就继续send会导致两条消息被合成一条发过去,具体可以在测试DEMO的时候频繁按下某键看结果
因为RM的限制Graphics.update会在游戏处于后台的时候卡住游戏,所以当游戏处于后台时socket操作全部被暂停,积下没有收到的信息会在一次recv过程中合并成一条消息接收(后台运行可破之
具体代码可以参考DEMO
顺便一提核心脚本在DEMO脚本页的第一项……忘了写名字
顺便用vLan和别人联机的时候死活收不到消息,明明用家里的两台机子一台做主另一台挂上德国的VPN通过vLan去连都能连上的……
DEMO的使用方法是在连接以后按下游戏中能够按下的按键(ZXC上下左右之类),然后另一端会收到按键被按下的消息

一些截图



DEMO下载
https://dl.dropboxusercontent.com/u/322483/socket.7z

核心脚本
RUBY 代码复制
  1. #encoding: utf-8
  2. module NET
  3.   #USER_DEFINED_CONST
  4.   MAX_BUFFER = 4096
  5.  
  6.   #const
  7.   WSADESCRIPTION_LEN = 256
  8.   WSASYS_STATUS_LEN = 128
  9.   AF_INET = 2
  10.   SOCK_STREAM = 1
  11.   IPPROTO_TCP = 6
  12.   INVALID_SOCKET = ~0
  13.   SOCKET_ERROR = -1
  14.   INADDR_ANY = 0x00000000
  15.   SOMAXCONN = 0x7fffffff
  16.   FIONBIO = 2147772030
  17.   EINPROGRESS = 10036
  18.   EISCONN = 10056
  19.   EWOULDBLOCK = 10035
  20.   EINVAL = 10022
  21.  
  22.   #win32api
  23.   API_WSAStartup = Win32API.new('ws2_32','WSAStartup','ip','i')
  24.   API_WSACleanup = Win32API.new('ws2_32','WSACleanup','v','i')
  25.   API_socket = Win32API.new('ws2_32','socket','iii','i')
  26.   API_htons = Win32API.new('ws2_32','htons','i','i')
  27.   API_ntohs = Win32API.new('ws2_32','ntohs','i','i')
  28.   API_htonl = Win32API.new('ws2_32','htonl','L','L')
  29.   API_inet_addr = Win32API.new('ws2_32','inet_addr','p','L')
  30.   API_inet_ntoa = Win32API.new('ws2_32','inet_ntoa','L','p')
  31.   API_ioctlsocket = Win32API.new('ws2_32','ioctlsocket','iip','i')
  32.   API_bind = Win32API.new('ws2_32','bind','ipi','i')
  33.   API_listen = Win32API.new('ws2_32','listen','ii','i')
  34.   API_accept = Win32API.new('ws2_32','accept','ipp','i')
  35.   API_connect = Win32API.new('ws2_32','connect','ipi','i')
  36.   API_recv = Win32API.new('ws2_32','recv','ipii','i')
  37.   API_send = Win32API.new('ws2_32','send','ipii','i')
  38.   API_closesocket = Win32API.new('ws2_32','closesocket','i','i')
  39.   API_WSAGetLastError = Win32API.new('ws2_32','WSAGetLastError','v','i')
  40.  
  41.   def self.init
  42.     #最后两个char是为了内存对齐(byte alignment)理论上说并非必须
  43.     wsdata_types = "SSc#{WSADESCRIPTION_LEN+1}c#{WSASYS_STATUS_LEN+1}SSLcc"
  44.     wsadata = ([0]*(WSADESCRIPTION_LEN+WSASYS_STATUS_LEN+9)).pack(wsdata_types)
  45.     version = [2,2].pack("CC").unpack("S")[0]
  46.  
  47.     if API_WSAStartup.call(version,wsadata) != 0
  48.       false
  49.     else
  50.       @inited = true
  51.     end
  52.   end
  53.  
  54.   def self.inited?
  55.     return @inited
  56.   end
  57.  
  58.   class Socket
  59.     attr :state
  60.     attr :errcode
  61.     attr :accepted_sockets
  62.     def initialize
  63.       @state = :not_ready
  64.       @errcode = 0
  65.       @accepted_sockets = []
  66.       return false unless NET.inited?
  67.  
  68.       @socket = API_socket.call(AF_INET,SOCK_STREAM,IPPROTO_TCP)
  69.       if @socket == INVALID_SOCKET
  70.         return false
  71.       end
  72.  
  73.       flag = [1].pack('L')
  74.       if API_ioctlsocket.call(@socket,FIONBIO,flag) == SOCKET_ERROR
  75.         return false
  76.       end
  77.  
  78.       @state = :ready
  79.     end
  80.  
  81.     def connected?
  82.       return @state == :connected
  83.     end
  84.  
  85.     def error?
  86.       return @state == :error
  87.     end
  88.  
  89.     def sending?
  90.       return @state == :sending
  91.     end
  92.  
  93.     def listening?
  94.       return @state == :listening
  95.     end
  96.  
  97.     def update
  98.       @recv = nil
  99.       @accepted_sockets.each{ |cs| cs.update }
  100.       case @state
  101.       when :connecting
  102.         if @connect_info.nil?
  103.           @state = :ready
  104.         else
  105.           if API_connect.call(@socket,@connect_info,@connect_info.size) == SOCKET_ERROR
  106.             ref = API_WSAGetLastError.call
  107.             if ref == EISCONN
  108.               @state = :connected
  109.             elsif ref != EWOULDBLOCK && ref != EINPROGRESS && ref != EINVAL
  110.               puts "Error occured when connecting"
  111.               @state = :error
  112.               @errcode = ref
  113.             end
  114.             p ref
  115.           else
  116.             @state = :connected
  117.           end
  118.         end
  119.  
  120.       when :connected
  121.         @buf = "\0"*MAX_BUFFER if @buf.nil?
  122.         if (response_size = API_recv.call(@socket,@buf,@buf.size,0)) == SOCKET_ERROR
  123.           ref = API_WSAGetLastError.call
  124.           if(ref != EWOULDBLOCK)
  125.             @state = :error
  126.             @errcode = ref
  127.           end
  128.         else
  129.           @recv = @buf[0...response_size]
  130.         end
  131.  
  132.       when :sending
  133.         if API_send.call(@socket,@message,@message.size,0) == SOCKET_ERROR
  134.           ref = API_WSAGetLastError.call
  135.           if ref != EWOULDBLOCK
  136.             @state = :error
  137.             @errcode = ref
  138.             puts "Error occured when sending"
  139.           end
  140.         else
  141.           @state = :connected
  142.         end
  143.  
  144.       when :listening
  145.         @ci = '\0'*16 if @ci.nil?
  146.         len = [@ci.size].pack('i')
  147.         if (cs = API_accept.call(@socket,@ci,len)) == SOCKET_ERROR
  148.           ref = API_WSAGetLastError.call
  149.           if ref != EWOULDBLOCK
  150.             @state = :error
  151.             @errcode = ref
  152.           end
  153.         else
  154.           @accepted_sockets.push NET::ClientSocket.new({:socket => cs,
  155.                                                         :client_info => @ci})
  156.         end
  157.       end
  158.  
  159.  
  160.     end
  161.  
  162.     def connect(ip,port)
  163.       p ip,port.to_i
  164.       return false if @state != :ready
  165.  
  166.       sockaddr_in = [AF_INET,API_htons.call(port.to_i),API_inet_addr.call(ip)]
  167.       sockaddr_in += ([0]*8)
  168.       @connect_info = sockaddr_in.pack('sSLc*8')
  169.       @state = :connecting
  170.  
  171.     end
  172.  
  173.     def send(message)
  174.       return false if @state != :connected && @state != :sending
  175.       @message = message
  176.       if API_send.call(@socket,@message,@message.size,0) == SOCKET_ERROR
  177.         ref = API_WSAGetLastError.call
  178.         if ref != EWOULDBLOCK
  179.           puts "Error occured when send"
  180.           @state = :error
  181.           @errcode = ref
  182.         else
  183.           @state = :sending
  184.         end
  185.       else
  186.         @state = :connected
  187.       end
  188.     end
  189.  
  190.     def recv
  191.       return false if @state != :connected
  192.       return @recv
  193.     end
  194.  
  195.     def listen(port)
  196.       return if @state != :ready
  197.       #server info
  198.       sockaddr_in_server = [AF_INET,API_htons.call(port),API_htonl.call(INADDR_ANY)]
  199.       sockaddr_in_server += ([0]*8);
  200.       server_info = sockaddr_in_server.pack('sSLc*8')
  201.  
  202.       #bind
  203.       if API_bind.call(@socket,server_info,server_info.size) == SOCKET_ERROR
  204.         return false
  205.       end
  206.  
  207.       #listen
  208.       if API_listen.call(@socket,SOMAXCONN) == SOCKET_ERROR
  209.         return false
  210.       end
  211.  
  212.       @state = :listening
  213.  
  214.     end
  215.  
  216.     def stop_listening
  217.       @state = :listening_stoped
  218.     end
  219.  
  220.     def continue_listening
  221.       @state = :listening
  222.     end
  223.  
  224.   end
  225.  
  226.   class ClientSocket < Socket
  227.     attr :ip
  228.     attr :port
  229.     def initialize(params)
  230.       @state = :not_ready
  231.       @accepted_sockets = []
  232.       return false unless NET.inited?
  233.       @state = :connected
  234.       @socket = params[:socket]
  235.       @connect_info = params[:client_info]
  236.       info = @connect_info.unpack('sSLc*8')
  237.       @ip = API_inet_ntoa.call(info[2])
  238.       @port = API_ntohs.call(info[1])
  239.     end
  240.   end
  241.  
  242. end

作者: 晴兰    时间: 2014-2-15 01:32
提示: 作者被禁止或删除 内容自动屏蔽
作者: Sion    时间: 2014-2-15 10:10
谢谢分享。
作者: end55rpg    时间: 2014-2-15 10:26
好东西尤物收藏啦!
合并消息如果后台太久会不会一次性卡掉!?
能否有个消息接受底栏发黄光。。。。。
另外不知是不是wlan同一个连不上是ip,貌似要用内网ip才行,(以前测试的。。)

作者: 逸豫    时间: 2014-2-17 21:35
晴兰 发表于 2014-2-15 01:32
看到小瞳前辈这个帖子很是兴奋,于是想和其他人分享和介绍一下感觉,如有冒犯请见谅和不吝指正:

前辈的这 ...

1、叫我小瞳的人都是我前辈
2、HTTP应该有更加方便的库可以使用(网络编程了解不深,只用过Ruby的标准库,不能提供更详细的信息
3、DLL什么的虽然看不懂但是好高端的样子,我之所以想用纯Ruby写Win32API调用就是不熟悉DLL的写法……否则的话用WSAEventSelect搭配新的窗口过程效率也会更高
4、RM是单线程运行,select的时候会卡住游戏,也可能会有人希望在等待链接的时候自己去转个菊花什么的……所以用阻塞式的方法总觉得不太舒服……
作者: 晴兰    时间: 2014-2-18 00:29
提示: 作者被禁止或删除 内容自动屏蔽
作者: kinrwolf    时间: 2014-4-15 16:51
好帖,喜欢,真心觉得能联机共斗才是RM的道路
作者: yagami    时间: 2014-4-19 12:56
前段时间也在用scoket 关于发包收包  我用到的情况是 如果包的大小超1024 会被路由器截断 并且 并不是按1024截断 比如1400的包 可能会被截成 800的 和600发过来 所以我在包尾巴上都加上个结束符字符串判定的 ,这样便于拼接包
其实数据还是阻塞性的好 这样不会出现连续send的情况  主线程跑个渲染 用个图片转圈圈 主线程里加个update 不断判断某个bool 变量的变化 scoket的那个线程完成通讯后变更那个bool变量的值 让主线程继续跑下去
作者: jxy    时间: 2014-4-28 12:33
不是说有va工程吗?怎么没有了?

作者: qz1301490279    时间: 2014-6-24 22:25
收藏。 表示对网络化有兴趣
作者: 英顺的马甲    时间: 2014-6-28 00:55
本人api无能,所以弱弱地问一下,要如何用脚本拿到本机ip?==
作者: 逸豫    时间: 2014-7-1 19:58
英顺的马甲 发表于 2014-6-28 00:55
本人api无能,所以弱弱地问一下,要如何用脚本拿到本机ip?==


我一直都是ipconfig来着……
  1. require("Win32API")
  2. COPY_MEMORY = Win32API.new("Kernel32","RtlMoveMemory","pii","0")
  3. def get_hostname
  4.   buf = "\0"*255
  5.   Win32API.new("ws2_32","gethostname","pi","i").call(buf,255)
  6.   return buf
  7. end
  8. def get_host(name)
  9.   buf = [0,0,0,0,0].pack("LLssL")
  10.   pointer = Win32API.new("ws2_32","gethostbyname","p","L").call(name)
  11.   COPY_MEMORY.call(buf,pointer,buf.size)
  12.   return buf
  13. end
  14. def put_ips(hostent)
  15.   buf = [0].pack("L")
  16.   ip_buf = [0,0,0,0].pack("C4")
  17.   cpp = hostent.unpack("LLssL")[-1]
  18.   COPY_MEMORY.call(buf,cpp,buf.size)
  19.   pointer = buf.unpack("L").first
  20.   while pointer != 0
  21.     COPY_MEMORY.call(ip_buf,pointer,ip_buf.size)
  22.     puts ip_buf.unpack("C4").join(":")
  23.     cpp += 4
  24.     COPY_MEMORY.call(buf,cpp,buf.size)
  25.     pointer = buf.unpack("L").first
  26.   end
  27. end
  28. put_ips(get_host(get_hostname))
  29. `pause`
复制代码
获取全部IP的代码
截图:

作者: a965645462    时间: 2014-7-14 17:55
链接失效。。
作者: 有丘直方    时间: 2016-10-2 16:18
本帖最后由 有丘直方 于 2016-10-3 18:10 编辑

创建好几个NET::Socket对象是不是就可以同时联多台机器?
我想知道的是能不能让一个socket同时连接很多个IP,send的时候就可以把消息送给很多台机器了?如果不能的话就要用for in end语句给每一个机器发一个消息很麻烦。
能不能不用破墙链接放DEMO啊。
作者: 逸豫    时间: 2017-1-15 16:41
有丘直方 发表于 2016-10-2 16:18
创建好几个NET::Socket对象是不是就可以同时联多台机器?
我想知道的是能不能让一个socket同时连接很多个IP ...

国内DEMO下载链接:
百度云:http://pan.baidu.com/s/1o8AqgGi
七牛:http://77g811.com1.z0.glb.clouddn.com/socket.7z
服务器端只需要listen一个socket即可接受其他所有客户端传入的请求,传入请求的socket会被存储在accepted_sockets中。当然,仍然需要通过遍历各个传入请求的socket来读取/发送数据。
本SOCKET通信采用的是TCP协议,TCP协议是一种P2P协议,无法通过广播的方式传递信息。




欢迎光临 Project1 (https://rpg.blue/) Powered by Discuz! X3.1