作者:老九—技术大黍
社交:知乎
公众号:老九学堂(新人有惊喜)
特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者授权
前言
世界最流行的Web服务器除了Apache之外,那就是Tomcat服务器了。而Tomcat是使用纯Java编写的Web服务器,它的底层的网络服务是使用Java的NIO和AIO技术实现的。
今天我们来讨论Java中书写游戏服务器必须使用的NIO技术。
ServerSocketChannel类
java.nio.channels包中的ServerSocketChannel抽象类是一个面向流监听的Socket的可选择通道。 该类的数字签名是:
public abstract class ServerSocketChannel extends AbstractSelectChannel
服务端的socket通道是不完整的监听网络socket的抽象。所以绑定和操作socket选择必须通过ServerSocket辅助对象完成—呼叫socket()方法即可。
它不可能给任意的、预存的服务端socket创建一个通道,也不可能使用一个服务端socket指定一个SocketImpl对象。 服务端Socket通道只能通过该类的open方法创建,但是它不能绑定主机和端口
如果不绑定该通道就呼叫socket的accept()方法,那么会抛出NotYetBoundException异常;
服务端socket通道呼叫服务端socket的bind方法来绑定主机和端口。
该类的父类AbstractSelectableChannel是一个抽象类,它实现Channel接口,是一个SelectableChannel实现。
AbstractSelectableChannel类处理机器的通道注册、释放和关闭。它维护该通道的当前锁定模式也就是当前选择Key的组。它执行所有需要的同步行为,以实现SelectableChannel的规范要求。
该类中定义的受保护方法没必要同步,因为其它线程可能会做相同的操作。
服务端演示代码
IServer接口
package com.funfree.game.chat;
import java.nio.*;
import java.nio.channels.*;
/**
功能:定义一个接口用来抽象网络服务器的操作
作者:技术大黍
备注:需要使用方来捕获异常。该接口的使用方式:
IServer server = NetManager.buildServer("实现类名");
*/
public interface IServer{
public void accept()throws Exception; //接收客户端的连接
public void read()throws Exception; //从客户端读取消息
public void write(SocketChannel channel, ByteBuffer writeBuffer)throws Exception; //向客户端写入消息
}
ChatterServer类
package com.funfree.game.chat;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;
import java.net.*;
import java.io.*;
import org.apache.log4j.*;
/**
功能:翻写一个聊天服务器,使用使用NIO实现的多人聊天应用
作者:技术大黍
备注:socket接受、从socket读和向socket写的方法必须写在接口定义的accept、read和write方法中
*/
public class ChatterServer extends Thread implements IServer{
private static final int BUFFER_SIZE = 255; //缓冲容量的大小
private static final long CHANNEL_WRITE_SLEEP = 10L; //通道写操作时的间隔时间
private static final int PORT = 10997; //端口号
private Logger log = Logger.getLogger(ChatterServer.class); //对该类进行日志操作
private ServerSocketChannel serverSocketChannel; //有服务端通道
private Selector acceptSelector; //接受连接选择器
private Selector readSelector; //读取选择器
private SelectionKey selectKey; //被选择的键
private boolean running; //标识是否在运行
private LinkedList clients; //使用链表保存
private ByteBuffer readBuffer; //使用字节缓冲读
private ByteBuffer writeBuffer; //使用字节缓冲写
private CharsetDecoder asciiDecoder; //字符解码
public static void main(String args[]) {
// 配置log4j
BasicConfigurator.configure();
// 初始化聊天服务器,然后启动定
ChatterServer cs = new ChatterServer();
cs.start();
}
public ChatterServer() {
clients = new LinkedList(); //使用链表保存客户端
readBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
writeBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
asciiDecoder = Charset.forName( "gb2312").newDecoder();
}
/**
在这个方法中,服务端完成socket的创建、绑定(bind)本地端口,以及实现监听(Listen)的动作。
*/
private void initServerSocket() {
try {
// 指定打开非锁定的服务端socket通道
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
// 绑定本监听的端口
InetAddress addr = InetAddress.getLocalHost();
serverSocketChannel.socket().bind(new InetSocketAddress(addr, PORT));
// 获取选择器,用来处理多个客户端通道
readSelector = Selector.open();
}
catch (Exception e) {
log.error("初始化服务器出错", e);
}
}
public void run() {
initServerSocket();
log.info("聊天服务器正在运行。。。");
running = true;
int numReady = 0;
// 在连接客户端时锁定,直到有客户端连接
while (running) {
try{
// 检查新的客户端连接
accept();
// 检查接收的消息
read();
}
catch(Exception e){
log.error("出现错误:",e);
}
// 第间隔100毫秒接受连接和消息
try {
Thread.sleep(100);
}catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
/**
在这个方法,服务端是在完成了与客户端三次握手成功之后,第一次向客户端发送问候消息!
*/
public void accept() throws Exception{
//定义一个socket通道
SocketChannel clientChannel;
//因为serverSocketChannel是非锁定的,所以它会立即返回,而不管该连接是否要用
while ((clientChannel = serverSocketChannel.accept()) != null) {
addNewClient(clientChannel);
log.info("从: " + clientChannel.socket().getInetAddress() + "获取连接。");
sendBroadcastMessage("从:" + clientChannel.socket().getInetAddress() + "登录", clientChannel);
sendMessage(clientChannel, "\n\n欢迎来到聊天室,当前有" +
clients.size() + "用户在线。");
sendMessage(clientChannel, "输入 'quit'退出系统。");
}
}
/**
该方法是在三次握手,并且第一次回复客户端之后的双向socket交互循环行为!
*/
public void read() throws Exception{
// 使用非锁定选择,所以会立即返回,而不管有多少个key在准备
readSelector.selectNow();
// 获取key值
Set readyKeys = readSelector.selectedKeys();
// 通过key和进程来运行
Iterator i = readyKeys.iterator();
while (i.hasNext()) {
SelectionKey key = (SelectionKey) i.next();
i.remove();
SocketChannel channel = (SocketChannel) key.channel();
readBuffer.clear();
// 把通道中内容读到缓冲
long nbytes = channel.read(readBuffer);
// 检查流结束符
if (nbytes == -1) {
log.info("断开连接: " + channel.socket().getInetAddress() + ",因为遇到流结束符。");
channel.close();
clients.remove(channel);
sendBroadcastMessage("用户:" + channel.socket().getInetAddress() + "登出", channel);
}
else {
// 把附加放到字符中缓冲
StringBuffer sb = (StringBuffer)key.attachment();
//使用字符集解码器把这些字节解析成字符串,然后附加到字符串缓冲
readBuffer.flip( );
String str = asciiDecoder.decode( readBuffer).toString( );
readBuffer.clear( );
sb.append( str);
// 检查所有的内容行
String line = sb.toString();
if ((line.indexOf("\n") != -1) || (line.indexOf("\r") != -1)) {
line = line.trim();
if (line.startsWith("quit")) {
// 如果是quit消息,那么判决该通道,并把它从在线用户中删除
log.info("得到退出(quit)消息,现在关闭:" + channel.socket().getInetAddress());
channel.close();
clients.remove(channel);
sendBroadcastMessage("用户: " + channel.socket().getInetAddress() + "退出", channel);
}
else {
// 否则向所有客户广播消息
log.info("广播: " + line);
sendBroadcastMessage(channel.socket().getInetAddress() + ": " + line, channel);
sb.delete(0,sb.length());
}
}
}
}
}
private void addNewClient(SocketChannel chan) throws Exception{
// 添加socket通道到集合中
clients.add(chan);
//使用选择器注册该通道,保存了新的字符串缓冲作为Key的附件,该附件用来保存需要读取的消息
chan.configureBlocking( false);
SelectionKey readKey = chan.register(readSelector,
SelectionKey.OP_READ, new StringBuffer());
}
private void sendMessage(SocketChannel channel, String mesg)throws Exception {
prepWriteBuffer(mesg);
write(channel, writeBuffer);
}
private void sendBroadcastMessage(String mesg, SocketChannel from)throws Exception {
prepWriteBuffer(mesg);
Iterator i = clients.iterator();
while (i.hasNext()) {
SocketChannel channel = (SocketChannel)i.next();
if (channel != from)
write(channel, writeBuffer);
}
}
private void prepWriteBuffer(String mesg) {
//从给定的字符串填充缓冲,以便一个通道执行写操作
writeBuffer.clear();
writeBuffer.put(mesg.getBytes());
writeBuffer.putChar('\n');
writeBuffer.flip();
}
public void write(SocketChannel channel, ByteBuffer writeBuffer)throws Exception {
long nbytes = 0;
long toWrite = writeBuffer.remaining();
//循环执行通道的写操作,但是没有必要,因所有字节一下就可以写完
while (nbytes != toWrite) {
nbytes += channel.write(writeBuffer);
try {
Thread.sleep(CHANNEL_WRITE_SLEEP);
}catch (InterruptedException e) {
}
}
// 准备另外一个写操作
writeBuffer.rewind();
}
}
SocketChannel类
SocketChannel类是一个面向流的连接socket的选择通道。 它是数字签名如下:
public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
该类是一个不完整的连接网络socket的抽象,因此需要Socket对象的协助完成绑定主机,关闭连接,以及操作Socket选项等。
Socket通道对象只能通过该类的open方法创建。一个新建的Socket通道是打开的,但是没有连接到服务器。如果现有呼叫通道的I/O操作会抛出NotYetConnectedException异常。
当呼叫SocketChannel.connect()方法时,通道会连接到服务端,并且它的生命周期直到呼叫SocketChannel.close()方法时停止。判断通道是否连接到服务器时,呼叫isConnected()方法。
Socket通道支持非锁定连接:一个socket通道可以先创建,然后初始使用connect连接到远程socket对象,最后finishConnect方法完成最后的连接。
确定当前连接操作是否正在进行,呼叫isConnectionPending方法即可。向一个socket通道执行读写操作,可独立的执行“shut down”(关闭)动作,但是不实际关闭通道本身。关闭输出通道呼叫shutdownInput方法,返之呼叫shutdownOutput方法;如果再次执行写通道操作,会抛出CloseChannelException异常。
Socket通道支持异步关闭,类似于Channel类的异常关闭操作。如果一个读socket线程关闭读动作,那么另外一个锁定读操作的线程,会马上结束读操作,然后返回-1值。写socket线程关闭操作,那么另外一个锁定写操作的线程会接收到AsynchrousCloseException异常。 Socket通道的多线程并发操作是安全的。
客户端演示代码
ChatterClient类
package com.funfree.game.chat;
import java.nio.*;
import java.nio.charset.*;
import java.nio.channels.*;
import java.util.*;
import java.net.*;
import java.io.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
/**
功能:翻写一个聊天客户端,使用使用NIO实现的多人聊天应用
作者:技术大黍
*/
public class ChatterClient extends Thread implements IServer{
private static final int BUFFER_SIZE = 255;
private static final long CHANNEL_WRITE_SLEEP = 10L;
private static final int PORT = 10997;
private ByteBuffer writeBuffer;
private ByteBuffer readBuffer;
private boolean running;
private SocketChannel channel;
private String host;
private Selector readSelector;
private CharsetDecoder asciiDecoder;
private InputThread it;
public static void main(String args[]) {
String host = args[0];
ChatterClient cc = new ChatterClient(host);
cc.start();
}
public ChatterClient(String host) {
this.host = host;
writeBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
readBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
asciiDecoder = Charset.forName( "gb2312").newDecoder();;
}
public void run() {
try{
accept(); //连接服务器
it = new InputThread(this);//开启一个输入线程,读取键盘输入
it.start();
running = true;
//判断当前客户端是否正常运行
while (running) {
read(); //从读取服务端的消息
// 每50毫秒从服务器读一次消息
try {
Thread.sleep(50);
}
catch (InterruptedException ie) {
}
}
}catch(Exception e){
e.printStackTrace();
}
}
/**
在这个方法中完成创建socket对象、绑定(bind)本地端口,以及连接(connect)到服务端
*/
public void accept() throws Exception{
//打开读取选择器
readSelector = Selector.open();
InetAddress addr = InetAddress.getByName(host);
channel = SocketChannel.open(new InetSocketAddress(addr, PORT));
channel.configureBlocking(false);
//注册通道,把内容放到字符串缓冲
channel.register(readSelector, SelectionKey.OP_READ, new StringBuffer());
}
/**
在这个方法中,客户端完成向服务端发送一个回复accept()动作的消息。
在selectedKey集合中,就是与服务端握手成功之后的交互!
*/
public void read() throws Exception{
// 检查是否有服务端的消息
// 因为是非锁定选择器,所以会立即返回,而不有多少key准备读
readSelector.selectNow();
// 获取key
Set readyKeys = readSelector.selectedKeys();
// 通过key和进程进行运行
Iterator i = readyKeys.iterator();
while (i.hasNext()) {
SelectionKey key = (SelectionKey) i.next();
i.remove();
SocketChannel channel = (SocketChannel) key.channel();
readBuffer.clear();
// 把通道中的内容读到缓冲
long nbytes = channel.read(readBuffer);
// 检查是否流结束
if (nbytes == -1) {
System.out.println("断开与服务器的连接,因为流结束了");
channel.close();
shutdown();
it.shutdown();
}
else {
// 否则获取附件信息,然后放到字符串缓冲
StringBuffer sb = (StringBuffer)key.attachment();
//使用CharsetDecoder把获取的字节转换成字符串,然后附加到字符串缓冲
readBuffer.flip( );
String str = asciiDecoder.decode( readBuffer).toString( );
sb.append( str );
readBuffer.clear( );
// 检查整行,然后输出控制台
String line = sb.toString();
if ((line.indexOf("\n") != -1) || (line.indexOf("\r") != -1)) {
sb.delete(0,sb.length());
System.out.print("" + line);
System.out.print("> ");
}
}
}
}
private void sendMessage(String mesg) throws Exception{
prepWriteBuffer(mesg);
write(channel, writeBuffer);
}
private void prepWriteBuffer(String mesg) throws Exception{
//使用给定的字符串填充到缓冲,以便通道执行写操作
writeBuffer.clear();
writeBuffer.put(mesg.getBytes());
//writeBuffer.putChar('\n');
writeBuffer.flip();
}
public void write(SocketChannel channel, ByteBuffer writeBuffer) throws Exception{
long nbytes = 0;
long toWrite = writeBuffer.remaining();
// 进行写操作
while (nbytes != toWrite) {
nbytes += channel.write(writeBuffer);
}
// 准备下一次写操作
writeBuffer.rewind();
}
public void shutdown() {
running = false;
interrupt();
}
/**
* 功能:该线程完成键盘的输入功能
*/
class InputThread extends Thread {
private ChatterClient cc;
private boolean running;
public InputThread(ChatterClient cc) {
this.cc = cc;
}
public void run() {
/*JFrame chatFrame = new JFrame("聊天客户端");
chatFrame.setSize(300,250);
chatFrame.setLocationRelativeTo(null);
chatFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
chatFrame.setVisible(true);
final JTextArea text = new JTextArea(15,15);
JButton sendButton = new JButton("发送");
chatFrame.add(text,BorderLayout.CENTER);
chatFrame.add(sendButton,BorderLayout.SOUTH);
sendButton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
String content = text.getText().trim();
try{
if (content.length() > 0){
cc.sendMessage(content + "\n");
}
if(content.equals("quit")){
cc.shutdown();
}
}catch(Exception ee){
ee.printStackTrace();
}
}
});*/
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
running = true;
while (running) {
try {
String s;
System.out.print("> ");
System.out.flush();
s = br.readLine();
if (s.length() > 0)
cc.sendMessage(s);
if (s.equals("quit"))
running = false;
}catch (Exception ioe) {
running = false;
}
}
cc.shutdown();
}
public void shutdown() {
running = false;
interrupt();
}
}
}
Selector类
Selector类是一个SelectableChannel对象的多路复用器。 它的数字签名是:
public abstract class Selector extends Object
一个选择器对象可以由该类的open方法创建,该类用来使用操作系统提供的默认选择器提供者,以便创建一个新的选择器。一个选择器对象也可以呼用户自定义的selector提供者的openSelector方法来创建。
选择器会一直打开,直到呼叫close方法时关闭。当一个可选择的通道使用选择器进行注册时,那么会产生SelectionKey对象。选择器会维护三组选择key:
- key set (键集合)—这些key表示当前注册了该选择器的通道。呼叫Selector.keys()方法返回
- selected-key(选择了的键集合)—表示被系统侦查到的,已经准备好的通道。呼叫Selector.selectedKeys()方法返回,它是keys的子集
- cancelled-key(取消的键集合)—表示已经注销的通道对象,这个集合不能直接访问。
在创建一个新的选择器时,三个集合都是空的。
放到选择器的key集合中的key就是通过通道注册了的有效通道对象。取消的key会在key操作期间被删除,因为key集合本身不能直接被修改。取消key时会导致它所属通道被注销。被添加到选择了key集合中的key对象,可以呼叫Set.remove()方法移动掉。Key不可以直接被添加到选择了的集合(Set)中去,Selector类自我实现多个线程的并发操作,包括它的key集合。选择器本身的选择操作也是同步的,不管是针对选择了key集合,还是取消了key集合。线程锁定select()和select(long)操作可以三种方式终止:
- 呼叫Selector.wakeup方法
- 呼叫Selector.close方法
- 呼叫锁定线程的interrupt方法
SelectionKey类
SelectionKey是一个抽象类,它表示一个使用Selector注册成功的SelectableChannel通道。它的数字签名如下
public abstract class SelectionKey extends Object
一个选择key在一个通道使用一个选择器注册成功之后被创建。一个的key的生命周期存活到呼叫SelectionKey.cancel()方法时为止,或者当Selector.close()方法时为止。呼叫SelectionKey.cancel()方法是,不是立即把该通道删除,而是放到Selector的取消key集合中保存,让Selector去销毁。该类的选择并发操作是安全的,对于读写interest集合是同步的。在实际开发中,使用应用级别的数据结构,比如使用一个对象表示高阶的协议,这样可实现协议顺序读取性。因此SelectionKey支持使用任意对象作为附件扔给这个key使用。作为附件的对象,在呼叫SelectionKey.attach()方法添加到通道,获取附件时呼叫attachment方法即可。
ByteBuffer类
该类是一个字节缓冲的抽象,它的数字签名如下:
public abstract class ByteBuffer extends Buffer implements Comparable
该类定义了六种操作字节的操作:
- 绝对和相对的get和put操作,这些操作是单个字节的读和写操作
- 相对的块get方法,把连接了字节传送到一个数组中去
- 相对的块put方法,是get相反操作
绝对和相对的get和put方法,用来读写其它原始数据类型。
字符集
java.nio.charsets包定义了新的字符集和新的字符转码的机制。新的字符集类以更标准的方式来表示以上抽象的概念。字符集(Character set)—字符的集合,比如字母”A”是字符,同样”%”也是字符;不管直接数字值,或者ASCII值、Unicode值都是字符。它们在计算机被发明之前就已经存在了。编码字符集(Coded character set)—一组表示字符集的指定数值。这些指定的数值可以数字化的表示特定字符编码集。其它的编码字符集合,可能会用不同的数值表示相同的字符。比如US-ASCII码,ISO 8859-1编码, Unicode (ISO 10646-1)编码等。 以上三种编码就是把编码字符集的成员影射成字节(8位)序列。而编码策略定义了怎样编码字符的序列,该序列是一个字节的序列。它有点像序列化和反序列的动作。
字符数据通常是被编码过的,用来通过网络传输,或者保存存文件形式。而编码策略不是字符集,它只是一种影射机制!(An encoding scheme is not a character set, it’s a mapping)比如UTF-8是一个专用的于Unicode字符集的编码策略,但是,它同样可以用来处理多种字符集。UTF-8编码字符代码值都小于0x80值,这个值被看成一个字节的值(标准ASCII码值)。其它的Unicode字符使用两个字节(16位)序列来编码。字符(Charset)—该术语是编码字符集和字节编码策略的综合体。java.nio.charset包就是它的抽象。
下面图示说明java.nio.charsets包是怎样封装字符的:
大多数操作系统还是使用面向字节的I/O操作与文件存贮方式,所以不管使用什么编码方式,比如Unicode编码或者其它的编码,它们都必须在字节序列与字符集编码之间转换! java.nio.charset包就是为了解决这个问题而产生的。它不是Java平台描述的字符集编码,它是最系统的、复杂的和灵活的解决方案。 java.nio.charset.spi包提供了服务提供者接口(SPI),该接口允许加入新的编码器和解码器。
Charset类
Charset 类封装了固定的信息,对于字符编码,我们使用Charset类来进行编码(encode)与解码(decode) 对于非阻塞方式的socket编程,必须使用Charset类、CharsetDecoder类、CharsetEncoder类来对二进制位信息进行编码与解码,才能正确的发送与显示。
Charset演示代码
package com.funfree.game.chat;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;
import java.net.*;
import java.nio.channels.spi.*;
import java.nio.charset.*;
/**
功能:书写一个类最简化版本的NIO服务端
作者:技术大黍
备注:使用单线程完成Socket的客户端的并发处理
*/
public class NIOChatServer{
private ServerSocketChannel serverChannel;
private Selector connectSelector;
private PrintWriter out = new PrintWriter(System.out,true);
private static int PORT = 1818;
/**
在构造方法中初始化成员变量,以及完成监听客户端的接入动作
*/
public NIOChatServer()throws Exception{
//创建一个非阻塞服务端通道
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
//创建一个选择器,用来注册服务端通道
connectSelector = SelectorProvider.provider().openSelector();
//绑定服务端通道到指定的地址和端口
/*InetSocketAddress address = new InetSocketAddress(
InetAddress.getByName("az-1"),PORT);*/
serverChannel.socket().bind(new InetSocketAddress(PORT));
//注册服务端通道,表示等待新的连接
serverChannel.register(connectSelector, SelectionKey.OP_ACCEPT);
//启动一个子线程来处理所有的并发的客户端请求
new ClientHandler(connectSelector).start();
out.println("服务器已启动。。。");
}
public static void main(String[] args){
try{
new NIOChatServer();
}catch(Exception e){
e.printStackTrace();
}
}
}
/**
功能:使用一个子线程专门处理与客户端的通讯交流
作者:技术大黍
备注:处理并发是通过key来判断操作,所有的客户端使用一个SocketChannel来表示。
非阻塞技术与普通I/O流技术的区别是:使用一个线程处理多个客户端的socket
对象;而不是给每个客户端的socket分配一个线程来处理!
*/
class ClientHandler extends Thread{
static LinkedList clients = new LinkedList(); //用来保存客户端socket对象
private Selector selector;
static int count = 0;
private PrintWriter out = new PrintWriter(System.out, true);
static boolean flag = false; //false表示没有初始化过,true表示已经初始化过了
public ClientHandler(Selector selector)throws Exception{
this.selector = selector;
}
/**
在run方法中来判断是否有客户端的连接和消息的读入。
*/
public void run(){
try{
while(true){
//查看注册了的通道
selector.select();
Iterator selectedKeys = selector.selectedKeys().iterator();
while(selectedKeys.hasNext()){
SelectionKey key = (SelectionKey)selectedKeys.next();
selectedKeys.remove();
if(! key.isValid()){//如果没有找到适合的socket通道
continue; //不作为
}
//否则根据isXXXX来判断是客户端接入,还是客户端消息读入
if(key.isAcceptable()){//如果有客户端接入
//那么处理多个客户端接入的情况
accept(key);
}else if(key.isReadable()){ //如果有消息读入
sendAll(key);
}
}
sleep(100); //间隔100毫秒接收客户端的接入
if(!flag){
//定时向各个客户端发送消息
//new MyTimer().start();
}
}
}catch(Exception e){
e.printStackTrace();
}
}
private void accept(SelectionKey key)throws Exception{
ServerSocketChannel serverChannel = (ServerSocketChannel)key.channel();
//获取客户端连接
SocketChannel socketChannel = serverChannel.accept();
Socket socket = socketChannel.socket();//获取一个socket用来向客户端说Hello
PrintWriter output = new PrintWriter(socket.getOutputStream(),true);
output.println("欢迎进入聊天室!");
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ,new StringBuffer());
count++;
out.println("当前有" + count + "客户端连接到服务器。");
clients.addLast(socketChannel); //保存到集合中
}
private void sendAll(SelectionKey key)throws Exception{
SocketChannel client = (SocketChannel)key.channel();
client.configureBlocking(false); //如果这样设置为非阻塞,并且立即注册通道如下
client.register(selector, SelectionKey.OP_READ,new StringBuffer());//那么就是告诉操作系统立即读取当前所信息
ByteBuffer buffer = ByteBuffer.allocate(255);
int flag = client.read(buffer); //关键代码是SocketChannel.read方法
//因为系统感知的,所以需要通过客户端的输入来判断服务端的通讯内容。
if(flag == -1){
client.close();
clients.remove(client);
}else{
StringBuffer sb = (StringBuffer)key.attachment();
buffer.flip();
//在循环读入的过程,必须使用CharsetDecoder来处理接收的字符,否则不会出现
//我们想要的字符结果!!!
CharsetDecoder decoder = Charset.forName("gb2312").newDecoder();
//String str = new String(buffer.array());//不解码会出现问题
String str = decoder.decode(buffer).toString();
buffer.clear();
sb.append(str);
String line = sb.toString();
if((line.indexOf("\n") != -1) || (line.indexOf("\r") != -1)){
line = line.trim();
out.println("客户端输入的内容是:" + line);
if(line.startsWith("quit")){
client.close();
clients.remove(client);
}else{
for(int i = 0; i < clients.size(); i++){
SocketChannel x = (SocketChannel)clients.get(i);
if(x != client){
ByteBuffer tempBuffer = ByteBuffer.wrap((client.socket().getInetAddress() +
"说:" + line + "\n").getBytes());
//然后向客户端写内容
x.write(tempBuffer);
tempBuffer.flip();
}
}
sb.delete(0,sb.length());
}
}
}
}
class MyTimer extends Thread{
public MyTimer(){
ClientHandler.flag = true; //表示已经初始化过了
}
public void run(){
try{
while(true){
sleep(500);
for(int i = 0; i < clients.size(); i++){
SocketChannel x = (SocketChannel)clients.get(i);
ByteBuffer tempBuffer = ByteBuffer.wrap("测试我的计时器".getBytes());
//然后向客户端写内容
x.write(tempBuffer);
tempBuffer.flip();
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
}
游戏数据设计说明
在游戏的服务端,除了上述的非阻塞方式实现的高并发服务性能之外,还有一个非常重要的方面。那就是游戏数据的存贮问题,一般使用关系型数据库。主要用来存贮玩家数据、得分中高分值、游戏统计数据、同伴列表,以及其它游戏特定的数据。因为我们使用标准的Java应用服务器:GameServer类,所以不会使用J2EE应用服务器。
那么数据库器怎样选择?大多数程序都选择使用直接使用JDBC API来完成。它的好处是速度,并且如果我们使用的数据小的话,那么这是非常好的策略。如果不强调速度,并且数据量非常大的时候,最好使用ORM技术来实现,比如JDO、Hibernate以及iBats等框架技术。 MMORPG游戏服务器还有提供玩家可以查找好友列表的功能,以及与这些好友聊天功能。
查找好友功能包括:好友列表(与即时通讯的功能一样),比如当你的好友登入时,显示你在哪个游戏大厅,以及哪个服务器(区)有哪些人在。玩家匹配特性,根据玩家所在大厅中相关玩家相同技能、游戏性,或者其它关键的会议、聊天,以及游戏任务挑战等。聊天功能所有游戏必须的功能。基于聊天功能,我们可以让管理员把有关的用户踢出。以保证游戏世界中的玩家是正常的玩家。
服务器管理
每个应用至少需要一个错误日志和调试日志。另外,我们可能会需要一个聊天日志,游戏历史日志,连接/访问日志,或者游戏特定的日志。JDK 1.4在java.util.logging包中提供了新的日志API。不过实际上的标准是apache的log4j框架。Log4j是高配置,易使用和高效率的框架。在游戏的客户端生成日志是非常重要的,我们需要看到数据,但是在测试阶段是非常重要的。即使在游戏布置之后,如果用户有麻烦,我们可以让客户端发送他/她的日志来帮助我们分析问题。
服务器管理员控制台:管理控制台可以远程的,也可以本地的应用。本地应用可以RPSConsoleEventReader类表示,它从键盘读入命令,然后把这些命令输入到事件中去。远程控制可以通过GameClient和RPSClient修改而成。当然可以控制或者GUI界面的。不管哪种方式,都要把GameEvent发送到GameController中去,类似于另外一个客户端。
不同的是,我们需要用户经过管理员的列表和远程IP地址过滤之后。管理员客户端可以与普通的GameController或者特定的AdminController讲话。一个AdminController类提供管理功能,比如服务器性能监视、游戏监控、日志查询,以及远程关闭/启动。对于安全要求,我们可以使用另外一个端口来管理接入,或者设置相应的防火强规则来限制端口访问。
游戏监控—是一种游戏在运行中的间谍行为,对于问题分析和玩家的行为分析是非常重要的。如果要实现该功能,那么修改GameClient类,对于用户的输入不需要响应。另外在GameController类需要修改,以便允许管理员客户端附加到游戏中,然后开始接收所有的事件。比如,sendEvent和sendBroadcastEvent方法需要先检查是否有间谍附件存在,然后把所有事件发送给游戏监控客户端。
断开连接和重新连接—在游戏服务器开发中最头痛是处理客户端断开连接。玩家可能因为多种原因掉线而退出游戏。比如网络繁忙,物理断开等情况。不管怎样,我们需要让玩家重新连接,然后继续游戏。对于已经发送了断开事件,并且已经关闭的连接,我们需要一个Reaper(收割者)来处理。这个收割者一个线程,它使用java.util.Timer或者用户自定义线程来监视已经发送了关闭事件,并且已经关闭的socket客户端,但是它们还没有被激活的socket客户端。 对于游戏服务器测试的指标有
- 服务器支持的最大并发用户数
- 并发连接之后的事件通过率(events/sec)
- 并发连接的延迟时间
- 并发连接之后的CPU使用情况
- 事件延迟情况(每次事件从接受到开始处理的时间)
- EventQueue积压率(用来调整Wrap池的大小)
- 内存使用时间,注意是否有连续增长情况,它表示有内存泄漏(使用Java会出现这种情况)
- 出现非正常异常和错误
- 线程死锁
测试时需要把客户端机器与服务端机器分开,因为客户端与服务端同在一台机器中是不可能出现并发的效果。该GameServer服务器有下面几个关键点是影响性能的:
- Event processing time(事件处理时间)
- Game logic times for each GameEvent type (每个游戏事件的游戏逻辑时间)
- Event latency (事件在队列中等待处理的时间)
- Database query times (数据查询次数)
- Time to add a new connection (每次连接时间)
在RPSController.processEvent方法可以添加统计代码优化性能时需要关注两点:
- Make events smaller—如果事件小,那么分配内存时间少,同时序列化时间少,最后它们的传输时间也少
- Send fewer events—尽可能少的发送事件。如果客户端不确实不需要数据,那么不需要发送给它们。比如GameController.sendBroadcastEvent()方法,我们不需要给事件源广播该消息。
另外我们需要优化GC—使用-Xincgc命令,以及JDK1.4以后使用-XX:+UseConcMarkSweepGC和-XX+UseParallelGC命令 参见:www.oracle.com/technetwork…
还有就是让对象重用—尽可能让少的对象来完成相同的事情。RPSController就是为达到这个目的设置的。总体上讲,使用对象池是最佳解决方案。比如GameServer能非常快速创建GameEvent实例。但是,这些事件的生命周期非常短,它们被创建,接着被发送到客户端,然后被销毁。这是使用c/c++的编程策略。 GameEvent池对游戏服务扩大的装载性能有着重大的影响,最难的部分是确定在这个池到底有多少个对象才合适,但是取决于自动测试结果,或者对象池可以被低级别是权限的线程来所捕获,然后对它的大小进行调整。 大多数Web和应用服务器都使用这样的技术来管理它们的请求处理线程池。 最后的优化策略—把线程保持到最小数目。使用Wrap模式可以保证使用较少的线程来检查平均队列大小。因为每个线程都需要额外的资源,所以我们应该把线程数量保存最小化。同步技术也是需要仔细考虑的,保持同步最小化这是不用多说的技术问题要避免这样的循环写法:
while(true){
checkForSomeCondition();
try{
Thread.sleep(SLEEP_TIME);
}catch(Exception e){}
}
最后,在循环中把所有不需要日志的代码统统删除掉。太多的日志会杀掉服务器的性能。
总结
说明到游戏服务器运行效率,那么一定与硬件相关,因此,Java必须给出运行效率较高API给第三方程序员使用。想写出高效的服务端代码,那么从Java的角度讲,掌握NIO编程技巧几乎是必须的。
如果大家感兴趣,想要完整的代码,那么使用下面的联系方式:
QQ咨询:胖达QQ:3038443845
微信加:laojiujun 老九君
最后
记得给大黍❤️关注+点赞+收藏+评论+转发❤️
作者:老九学堂—技术大黍
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。