在线的棋牌类网络游戏java服务端实现
星期六, 2013-01-19 | Author: Lee | JAVA-and-J2EE, webgame, 游戏开发 | 5,094 views
自己一直从事 页游开发看到一篇做游戏服务端的开发文章自然要收录下.(本文算是记录的蛮详细的,不过具体的性能优化,缓存,消息的处理等都不在其中,整体宏观列出了.)
本文来源于网络,好久之前看的,被收藏后一直没有看,今天看了后收藏到网站,原文地址:http://kakaluyi.iteye.com/blog/1489592.
一、网络
网络游戏,首先面临的问题当然是如何进行网络通信。首先考虑的是HTTP协议,因为所有的J2ME手机都支持这个,我们当然想尽可能的兼容用户。而且HTTP协议封装程度已经非常高了,不用去考虑线程、同步、状态管理、连接池,不过HTTP协议有两个不爽的地方:
◇ 协议无状态,这个问题已经困扰过很多人很多次了。我曾考虑过的解决办法是改造HTTP协议,在数据传输完成之后不关闭socket,但是这样做工作 量非常大,在项目周期中,基本上就是Mission impossible,不予考虑。那么客户也就只能通过轮询的方式向服务器请求数据。
◇ 网络流量过大。就这个项目来说,网络间传递的只是指令,但是每次传递都要加上一堆毫无用处的HTTP Head,再加上客户端需要做轮询,这个流量 对于手机来说简直恐怖,经简单测试,按照0.03元/K的GPRS网络费用计算,一局牌居然要消耗1元多的费用(每秒轮询),实在不可接受。也许我们可以 采用流量费包月的资费方式,不过这个话题与技术无关。
以上问题导致我们选择了Socket,这意味着我们将没有一个web环境,很多东西都要靠自己去实现:线程管理、客户状态监控、对象池、控制台……….
网络部分打算采用Java NIO来实现,这是一种新的网络监听方式,基于事件的异步通信,可以提高性能。每个客户端连接之后,会有一个独立的 SocketChannel与它通信,这个SocketChannel会在用户的整个生存周期中存在。用户如果断开连接,服务器会得到-1,并且会抛出 Connection reset异常,通过捕获这两个特征,可以在用户意外断开连接后清理相关的资源。由于NIO是异步通信的,所以没有复杂的线程管 理。
二、通信协议
这个项目并没有复杂的通信指令,命令数量很有限,但是还是有个关键问题需要关注:流量。为 了尽量减小流量,我们使用字节代替字符串来保存系统指令,这样可以使流量减少一半,比如使用一个字节来保存一张扑克牌,字节高位表示花色,字节低位表示数 字,如果0代表黑桃,那么黑桃三就应该是0x03,这个需要靠位操作来实现:
1 2 3 | int m=0; int n=3; byte card=(byte )(m)<<4)|((byte )n; //m左移四位,然后与n左或操作 |
游戏中需要传递用户的积分,这是一个大整数,使用四个字节来保存比较保险,将整数转换为四个字节的操作如下:
1 2 3 4 5 6 7 8 9 | public static byte [] translateLong(long mark) { byte [] b = new byte [4]; for (int i = 0; i < 4; i++) { b[i] = (byte ) (mark >>> (24 - i * 8)); } return b; } |
将四个字节转回来的操作如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public static long translateByte(byte [] b) { int mask = 0xff; int temp = 0; int res = 0; for (int i = 0; i < 4; i++) { res <<= 8; temp = b[i] & mask; res |= temp; } return res; } |
三、数据库连接池
由于没有一个web环境,所以我们需要自己实现一个数据库连接池,apache有一个项目叫做commons DBCP,这是一个基于apache自己 的对象池(apache commons pool)实现的数据库连接池,我们可以直接拿来使用,apache的软件未必是最好的,但是极大可能比我们自 己写的要好。
Commons DBCP需要三个.jar:
commons-collections-3.1.jar、commons-dbcp-1.2.1.jar、commons-pool-1.2.jar
这三个文件都可以在apache ? Jakarta ? commons项目下下载,加入到工程中即可。
构造一个数据库连接池的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import java.sql.Connection; import java.sql.SQLException; import org.apache.commons.dbcp.BasicDataSource; import org.apache.commons.dbcp.ConnectionFactory; import org.apache.commons.dbcp.DataSourceConnectionFactory; /** * * @author IA (//www.pomelolee.com) * @date 2013-1-19 下午3:57:21 */ public class SqlTools { private static BasicDataSource bds = new BasicDataSource(); private static ConnectionFactory fac = null; /** * @param args * @throws SQLException */ public static void main(String[] args) throws SQLException { // 初始化连接池 bds.setDriverClassName("org.postgresql.Driver "); // 数据库驱动程序 bds.setUrl("jdbc:postgresql://localhost:5432/myDB"); // 数据库url bds.setUsername("postgres"); // dba帐号 bds.setPassword("XXXXXXXX"); // 密码 bds.setInitialSize(100); // 初始化连接数量 bds.setMaxIdle(10); // 最大idle数 bds.setMaxWait(1000 * 60); // 超时回收时间 fac = new DataSourceConnectionFactory(bds); // 得到连接工厂 Connection conn = fac.createConnection(); // 从池中获得连接 conn.close(); // 释放连接,回到池中 // 销毁连接池 bds.close(); bds = null; fac = null; } } |
请自行处理操作中的各种异常。
四、扑克牌的生成
游戏中需要为用户生成随机的扑克牌,首先我们需要初始化一副牌,放到一个Hashmap中,每张牌以一个字节表示,高为代表花色,的为代表数字,生成整副牌:
1 2 3 4 5 6 7 8 9 10 11 12 13 | private static HashMap<Integer, Byte> cards = new HashMap<Integer, Byte>(); public static void pai() { int tmp = 0; for (int i = 0; i < 4; i++) { for (int m = 0; m < 13; m++) { tmp = ((byte) (i) << 4) | ((byte) m); // 使用位操作构造一张牌 cards.put(new Integer(i * 13 + m), new Byte((byte) tmp)); } } cards.put(new Integer(53), new Byte((byte) 0x4d)); // 大王 cards.put(new Integer(54), new Byte((byte) 0x4e)); // 小王 } |
如何随机地得到其中的N张牌呢?我们的做法是生成一个0-55的随机数,用这个随机数作主键从Hashmap中获得对象,取得之后,把该对象从队列中删 除,以免重复取得。由于java中的随机数是根据时间生成的,所以有可能导致用户得到的牌不够散,每个用户都摸到一条龙岂不是笑话?所以在生成随机数的时 候我们加入了一个大素数来作运算:
1 | long cardId=new Long ((Math.round(Math .random() * 87) % 55)).intvalue(); |
通过修改这个大素数,可以控制某个用户的牌比较好。
五、线程
实际上本系统并没有复杂的线程管理,但是我想提供一个控制台让管理员可以管理游戏主线程,可以让它停止、中段、恢复、重启动,本来的设计是管理员通过与 线程A打交道,通过A去管理主线程B,但是熟悉java线程的朋友都知道,线程互相管理基本上就是不实际的,举个最简单的例子,A如何销毁B?也许你会说 调用B的destroy()方法就好了,网上很多讲解java线程的资料也确实是这么说的,但是他们都是鬼扯的,自己去看看java源代码 吧,Thread.destroy()方法的实际代码如下:
1 2 3 4 | public void destroy() { throw new NoSuchMethodError (); } |
事实真相是,Thread.destroy()方法自始至终就没有被实现过。所有写文章,教别人用这个方法销毁线程的人,都去撞墙吧,丢人丢大了。
最好的办法是A负责生成一个B并且启动它,然后B自己管理生存周期,A和B通过使用可共享的方法来通信,这是sun推荐的做法。
六、异步消息
用户玩牌的过程中,有很多东西需要记录下来,比如记录用户的积分、等级变化,记录玩牌日志供数据统计等,当用户数量很多的时候,在数据库中记录这些信息 会很耗费资源,用户玩了一局之后会可能会等待很长时间。解决这个问题的方法是利用J2EE的消息bean来提供异步通信的机制,需要记录数据的时候,系统 会封装一个值对象,发送给J2EE容器,这个操作是很快的,完成之后就返回,用户可以继续操作,不用关心消息何时被处理。
J2EE的消息框架具备如下特征:
◇ 消息一定会被阅读,而且只阅读一次。JMS框架有自己的算法,把消息缓冲到硬盘,就算J2EE服务器死掉,消息也不会丢失。
◇ 系统采用点对点的Queue消息队列,可以保证同等优先级的消息先进先出。
在Jboss 4.0中,部署消息Bean和Queue队列,都比weblogic 8.1来的容易,只需要在jboss.xml中声明消息目的地,如 果jboss发现该目的地不存在的话,会自动建立一个,实在很简单。
七、启动与退出
为了让系统具备让 人满意的性能,应该尽量多的重用对象,减少创建新对象。比如上面提到的消息发送,我们的操作是提供一个静态类,在系统启动的时候就初始化,保持与JMS服 务器的连接,系统发送消息的时候,不用再去查询JNDI和生成QueueConnectionFactory,这样可以提高系统响应速度。
在数据库连接池的问题上,我们也采用同样的操作,启动的时候初始化N个连接。但是如果在关闭进程的时候不做任何操作,会导致JMS抛出socket异 常,虽然没什么大的影响,但总显得不专业,而且池中的连接不被释放的话,也可能导致问题。最好能够让系统像jboss等控制台程序一样,ctrl+c之后 能够执行操作,释放资源再退出。我们可以通过给进程/线程加上一个Hook来实现,windows程序员应该对这个非常熟悉。
Hook应该是一个线程方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 | public class Hook extends Thread { public void run() { //释放数据库连接,销毁连接池 //关闭与JMS的连接 } } //在主线程中加入: Runtime .getRuntime().addShutdownHook(new Hook()) ; |
那么进程/线程会在退出的时候执行Hook的run方法,清理资源。
文章作者: Lee
本文地址: https://www.pomelolee.com/1115.html
除非注明,Pomelo Lee文章均为原创,转载请以链接形式标明本文地址
No comments yet.
Leave a comment
Search
相关文章
热门文章
最新文章
文章分类
- ajax (10)
- algorithm-learn (3)
- Android (6)
- as (3)
- computer (85)
- Database (30)
- disucz (4)
- enterprise (1)
- erlang (2)
- flash (5)
- golang (3)
- html5 (18)
- ios (4)
- JAVA-and-J2EE (186)
- linux (143)
- mac (10)
- movie-music (11)
- pagemaker (36)
- php (50)
- spring-boot (2)
- Synology群晖 (2)
- Uncategorized (6)
- unity (1)
- webgame (15)
- wordpress (33)
- work-other (2)
- 低代码 (1)
- 体味生活 (40)
- 前端 (21)
- 大数据 (8)
- 游戏开发 (9)
- 爱上海 (19)
- 读书 (4)
- 软件 (3)