html5网络游戏开发历程(-)
http://blog.sina.com.cn/s/blog_6486ff4901010789.html
说来惭愧,从今年4月份开始启动的游戏,到现在已经有8个月了,而我居然还没有写点东西。想想第一个测试版本快出来了,是好是坏, 至少也出来了一个可以转得动的东西啦。写点经验,也顺便谈点感受。8个月的呕心沥血让我见识了下自己的编程能力,也让我认识到了自己的不足(包括管理和技 术方面的)。毕竟还是没有任何经验,出现的问题也比较多,自己都是采用最简单的方法去解决的。希望能给大家点启示。
首先谈一下我们的开发语言。前台网页主要是采用html5和javascript开发,没采用flash主要是考虑chrome不支持那些东 西,而html5是未来比较有情景的技术,并且chrome,safari,firefox等的最新版本基本都支持html5。后台用的是纯Java写 的。没有采用那些什么JavaBean,Serverlet那些东西。呵呵,主要是本人目前编程水平有限,没来得及去学习那些东西。所以,现在回头看看, 是不是应该再去学习一下那些东西,有时间还是要再看。数据库是采用mysql。然后再说一下这个开发环境,前台采用的是Dreamweaver CS5,他里面就支持html5。后台采用的是MyEclipse。最后再唠叨一下我们这个开发团队:我,龙哥,超哥,小宝,小波。呵呵,感谢他们在这8 个月中对我要随时开会的不厌其烦,也感谢他们在这8个月的开发过程中的付出与贡献。感谢他们,一生的团队。
刚开始接手这个项目的时候,大家都是一片空白,不知道该从何开始。当然技术得开始学,html5,javascript,都得从头学起。当我看完《 HTML5高级程序设计 》这本书的时候,我发现他的 WebSocketsAPI非常有特点,居然可以像socket那样使用,非常满足我这种游戏的需求(大数据量传输)。在这里,我认识到了首先得建立起前台与后台的通信的机制,这是非常重要的,也是必须的。不管其他的东西怎么改,这个都一样。认识到了这一点,我们下面首先开始谈游戏的通信机制。
Java后台怎么与前台进行通信?是用什么协议?http还是tcp,在我们这里,我选择了tcp。呵呵,主要是看到前台拥有类似于socket的东西。既然选择了tcp,然后又借了java方面的书开始猛翻,记得比较有用的好像是《 精通Java网络编程 》,看到里面的异步网络通信在实际项目中用得比较多,当时就采用了这种方式。当然我们不能直接将他拿过来用,毕竟,我们要完成的是一个比较实际的项目,而 不是一个拿来练手项目。我们就采用了一种异步通信加多线程的方式来作为我们的通信方式,因为这种方式能支持上万的用户同时在线,至少这目前已经满足了我们 的实际需求了。那本书中只提供了异步通信的方式,并没有提供如何与多线程的方式相结合起来。另外,异步通信如果与多线程相配合,也是一个我们值得去研究的 问题。比较经典的方式是要有一个线程专门负责读数据,有一个线程专门负责写数据,然后再有几个线程负责处理数据,我们当时就是采用这种。有了这个思想之 后,一顿百度狂搜,发现基本没有什么满足的资料,不知道现在能不能搜到了。而在google英文网站中,终于让我找到了一个差不多的例子,在这里共享给大 家,希望能大家以帮助。但是先声明,这是别人的(具体来源于哪个网址,我也忘记了),使用的时候要注意版权。
NBTest.java
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
public class NBTest {
private static final List<SelectionKey> keyPool = new ArrayList<SelectionKey>();
private static Selector selector = null;
public NBTest() {
}
@SuppressWarnings("unchecked")
public void startServer() throws Exception {
int channels = 0;
int nKeys = 0;
// 使用Selector
selector = Selector.open();
// 建立Channel 并绑定到9000端口
ServerSocketChannel ssc = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress("0.0.0.0", 9000);
ssc.socket().bind(address);
// 使设定non-blocking的方式。
ssc.configureBlocking(false);
// 向Selector注册Channel及我们有兴趣的事件
SelectionKey s = ssc.register(selector, SelectionKey.OP_ACCEPT);
printKeyInfo(s);
while (true) // 不断的轮询
{
debug("----------------------------------------------------------");
debug("NBTest: Starting select");
// Selector通过select方法通知我们我们感兴趣的事件发生了。
nKeys = selector.select();
//在select之后,当select动作进行时,可以将cancel的SelectionKey清除,避免在SelectionKey被Cancel后注册其对应的通道时发生异常(CancelledKeyException)
synchronized (keyPool) {
while(!keyPool.isEmpty()){
System.out.println("register new channle");
SelectionKey key = keyPool.remove(0);
SocketChannel sc = (SocketChannel) key.channel();
try {
sc.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);
}catch (Exception e) {
e.printStackTrace();
}
}
}
// 如果有我们注册的事情发生了,它的传回值就会大于0
if (nKeys > 0) {
debug("NBTest: Number of ready keys after select operation: " + nKeys);
// Selector传回一组SelectionKeys
// 我们从这些key中的channel()方法中取得我们刚刚注册的channel。
Set selectedKeys = selector.selectedKeys();
Iterator i = selectedKeys.iterator();
while (i.hasNext()) {
s = (SelectionKey) i.next();
printKeyInfo(s);
debug("NBTest: Keys in selector: "+ selector.keys().size());
// 一个key被处理完成后,就都被从就绪关键字(ready keys)列表中除去
i.remove();
if (s.isAcceptable()) {
// 从channel()中取得我们刚刚注册的channel。
Socket socket = ((ServerSocketChannel) s.channel())
.accept().socket();
SocketChannel sc = socket.getChannel();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
System.out.println(++channels);
}
if(s.isReadable()){
debug("NBTest: process read event");
Reader.addKeys(s);
debug("NBTest: read event after add Keys");
}
if(s.isWritable()){
debug("NBTest:process write event");
Writer.addKeys(s);
}
if(s.isReadable()||s.isWritable()){
s.cancel();
}
}
} else {
debug("NBTest: Select finished without any keys.");
}
}
}
public static void addPool(SelectionKey key){
synchronized (keyPool) {
keyPool.add(key);
if(selector != null){
selector.wakeup();
}
}
}
private static void debug(String s) {
System.out.println(s);
}
private static void printKeyInfo(SelectionKey sk) {
String s = new String();
s = "Att: " + (sk.attachment() == null ? "no" : "yes")+"\n";
s += ", Read: " + sk.isReadable()+"\n";
s += ", Acpt: " + sk.isAcceptable()+"\n";
s += ", Cnct: " + sk.isConnectable()+"\n";
s += ", Wrt: " + sk.isWritable()+"\n";
s += ", Valid: " + sk.isValid()+"\n";
s += ", Ops: " + sk.interestOps();
debug(s);
}
public static void main(String args[]) {
try {
for(int i=0;i<1;i++){
new Reader().start();
new Writer().start();
}
NBTest nbTest = new NBTest();
nbTest.startServer();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Reader.java
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
public class Reader extends Thread{
private static List<SelectionKey> keys = new ArrayList<SelectionKey>();
public static void addKeys(SelectionKey key){
synchronized (keys) {
System.out.println("add key : size="+keys.size());
keys.add(key);
keys.notifyAll();
}
}
public void run(){
System.out.println("reader thread launched");
while(true){
SelectionKey key = null;
synchronized (keys) {
if(keys.size()>0){
key = keys.remove(0);
}else{
try {
keys.wait();
continue;
} catch (InterruptedException e) {
continue;
}
}
}
read(key);
}
}
public void read(SelectionKey key){
while(true){
System.out.println("reading data");
SocketChannel sc = (SocketChannel) key.channel();
try {
ByteBuffer bf = ByteBuffer.allocate(1024);
int ret = sc.read(bf);
if(ret == -1){
System.out.println("client disconnected");
sc.socket().close();
sc.close();
return;
}else if(ret == 0){
//通道中无数据可读,应将当前通道交由Selector继续监视
NBTest.addPool(key);
return;
}
int readCount = bf.position();//或者ret
System.out.println("Reade bytes:"+readCount);
System.out.println("Current Position:"+bf.position());
bf.flip();
byte [] readedData = bf.array();
System.out.println("Readed Data:"+new String(readedData,0,ret));
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Writer.java
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
public class Writer extends Thread{
private static List<SelectionKey> keys = new ArrayList<SelectionKey>();
public static void addKeys(SelectionKey key){
synchronized (keys) {
System.out.println("add key : size="+keys.size());
keys.add(key);
keys.notifyAll();
}
}
public void run(){
System.out.println("reader thread launched");
while(true){
synchronized (keys) {
if(keys.size()>0){
SelectionKey key = keys.remove(0);
write(key);
}else{
try {
keys.wait();
} catch (InterruptedException e) {
continue;
}
}
}
}
}
public void write(SelectionKey key){
System.out.println("writing data");
SocketChannel sc = (SocketChannel) key.channel();
try {
ByteBuffer bf = ByteBuffer.allocate(1024);
bf.put("test send string".getBytes());
bf.flip();
int ret = sc.write(bf);
System.out.println("sended data");
Thread.sleep(5000);
NBTest.addPool(key);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
有了上面这个之后,后台的通信总算建立了。现在开始谈前台怎么通信的。刚刚说到前台html5有个类似于socket的东西,叫Websocket。如果调用了Websocket,是不是就能直接和后台进行通信呢?我当时也有同样的疑问,答案是否定的。而 《 HTML5高级程序设计 》也只是简单的提到怎么使用websocket,并没有给出具体的通信实例。一顿搜索之后,才发现websocket是一个协议,而目前支持他的有Node.js,Jetty。 后来又找到了一个Kaazing gateway,据说也支持websocket,并且还是一个可以免费使用的。然后就到起官网下载了一个kaazing-3.1.6,研究了一下他的开发 说明。修改了下里面conf文件夹下面的gateway-config.xml一些东西,开启斌文件夹下面的gateway.start.bat(如果启 动失败,说明你装的是jdk1.6版本以上,目前好像只支持1.5),然后测试了里面的web\samples\html5-javascript里面的 ws-completed.html,发现果然好使。呵呵,当时那个泪奔啊。。。至少证明前台和后台可以通信了。 gateway-config.xml里具体的修改也跟大家说一下,以免大家少走弯路。
在gateway-config.xml里面添加:
<service>
<accept>ws://localhost:8001/test</accept>
<connect>tcp://localhost:1234</connect>
<type>proxy</type>
<cross-site-constraint>
<allow-origin>http://localhost:8000</allow-origin>
</cross-site-constraint>
</service>
然后将ws-completed.html里面的location_.value = locationURI.toString(); 修改为 location_.value = "ws://localhost:8001/test";
当然你的服务器后台的端口也得修改为1234啊。应该就没什么大问题了。
好了,至此,html5网络游戏的通信机制算解决了。呵呵,不知道对大家有没有什么帮助。。。明日再写其他的。欢迎转载,希望注明出处:http://control.blog.sina.com.cn/myblog/htmlsource/article_preview.php