SocketChannel学习总结
这学期有一门课程需要做一些小设计,其中有一个21点游戏,我用SocketChannel做了联网,因为之前是没接触过SocketChannel的,所以是边学边做,
最近时间比较多,就做一下总结,以免忘记。
因为SocketChannel并没有提供很方便的多参数传输机制,它是直接传递字节流的,所以我制定了一个简单的协议来分辨一个字节流中的多个参数,这个协议的内容是除了int类型之外,其他类型的参数前面都必须加上一个整数,这个整数是参数化为字节数组的长度,那么我在接收的时候便可以根据每次先取出的这个整数来决定我接下来取出的这个参数要取多少字节。此外,Protocol_Config是将每一个功能对应一个整型数字,这样我在每次传输数据的时候就可以在开始先加上一个代表功能的整数,接收的时候根据这个数字来决定要将这次请求转交给哪个方法处理。以下是一个字节流的格式:
至于SocketChannel服务器与客户端的连接代码,网上也有很多,这里贴一下关键代码:
服务器:
/**
* 获得一个ServerSocket通道,并对该通道做一些初始化的工作
* @param port 绑定的端口号
* @throws IOException
*/
public void initServer(int port) throws IOException {
// 获得一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 获得一个通道管理器
this.selector = Selector.open();
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
//当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
* @throws IOException
*/
public void listen() throws IOException {
System.out.println("服务端启动成功!");
// 轮询访问selector
try{
SocketChannel temp = null;
while (true) {
//当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey selectionKey = (SelectionKey) ite.next();
//TODO 这里是为了捕获客户端的非正常关闭
try{
// 删除已选的key,以防重复处理
ite.remove();
// 客户端请求连接事件
if (selectionKey.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) selectionKey
.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
temp = channel;
System.out.println("取得和一个客户端的连接");
// 设置成非阻塞
channel.configureBlocking(false);
//在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。 ,或者说是对可读事件感兴趣
channel.register(this.selector, SelectionKey.OP_READ);
// 获得了可读的事件
} else if (selectionKey.isReadable()) {
read(selectionKey);
}
}catch(Exception e){
//特殊情况下关闭的客户端
if(temp!=null){
specialClose(temp);
selectionKey.cancel();
temp.socket().close();
temp.close();
System.out.println("关闭");
}
}
}
}
} catch(Exception e){
e.printStackTrace();
System.out.println("错误!:"+e.getMessage());
}
}
客户端:
//链接服务器
public boolean linkServer(){
try {
socketChannel = SocketChannel.open(new InetSocketAddress(hostIp,
hostListenningPort));
socketChannel.configureBlocking(false);
// 打开选择器
selector = Selector.open();
//本信道向选择器注册读权限
socketChannel.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
System.out.println("错误:"+e.getMessage());
return false;
}
ListenThread lt = new ListenThread(selector);
new Thread(lt).start();
return true;
}
//TODO 监听的线程
class ListenThread extends Protocol_DataExpress implements Runnable{
Selector selector;
public ListenThread(Selector selector){
this.selector = selector;
}
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("客户端监听开启成功");
try{
while (true) {
//当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator ite = selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
if (key.isConnectable()) {
System.out.println("由服务端主动发起的连接(没用到)");
SocketChannel channel = (SocketChannel) key
.channel();
// 如果正在连接,则完成连接
if(channel.isConnectionPending()){
channel.finishConnect();
}
// 设置成非阻塞
channel.configureBlocking(false);
//在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
channel.register(selector, SelectionKey.OP_READ);
// 获得了可读的事件
} else if (key.isReadable()) {
read(key);
}
}
}
}catch(IOException e){
System.out.println("错误:"+e.getMessage());
}
}
那么无论是服务器还是客户端,在接收到一个可读事件之后,都需要先取出一个整型数字,我命名为action,即动作,再根据这个动作将channel分配到相应的方法中处理:
/**
* 处理读取客户端发来的信息 的事件
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException{
//System.out.println("到达read方法");
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
//先获取动作码,判断要进行什么动作
int action = getInt(channel);
//System.out.println("action:"+action);
//这里根据action的范围去决定进入哪一个switch
//所以action划定好范围可以分别进入不同的中转站(switch)去选择执行相应的动作
if(action>=2000&&action<=2999)
switch(action){
case Protocol_Config.GET_CARD:
getPai(channel);break;
default:
other(channel);break;
}
if(action>=1000&&action<=1999){
//又一个switch
switch(action){
case Protocol_Config.CREATE_HOME:
createHome(channel);break;
case Protocol_Config.HOME_LIST:
sendHomeList(channel);break;
case Protocol_Config.PEOPLE_NUM:
sendPartNum(channel);break;
case Protocol_Config.INTO_HOME:
intoHome(channel);break;
case Protocol_Config.START_PAI:
startPai(channel);break;
case Protocol_Config.STOP_NEED:
stopNeedPai(channel);break;
case Protocol_Config.SHOW_PAI:
showPai(channel);break;
case Protocol_Config.SEND_MESS:
sendMess(channel);break;
case Protocol_Config.SEND_SCORE:
setRankScore(getString(channel),getInt(channel));break;
case Protocol_Config.EXIT:
exit(channel);break;
default:
other(channel);break;
}
}
}
//退房
public void exit(SocketChannel channel){
int homeNum;
try {
homeNum = getInt(channel);
Home home = getHomeByNum(homeNum);
sendRankScore(home.getCompetitor(channel));
if(home.getShenFen(channel)==1){
home.setHost(null);
}else if(home.getShenFen(channel)==2){
home.setPlayer(null);
}
channel.close();
if(home.getPartNum()==0){
list.remove(home);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
学了SocketChannel之后呢,感觉与之前做过的web项目相比,就是客户端请求之后并不需要再做其他事,而是由服务器接到这个请求之后再决定要不要发送请求给客户端,所以我感觉就是在为一个个可能接收到的请求写功能,发出请求与接收到请求可以是不连续的。而且,与http协议相比,socket 因为保持着长连接,所以服务器和客户端是都可以向对方发送请求的。此外,socketChannel 与普通的socket 相比呢,最大的好处是socketChannel是非阻塞的,它使用一个选择器Selector 来不断的轮询有没有注册的事件到达,如果有,Selector.select 就会有返回值,此时就可以取出事件来处理,这样我们就只需要在有事件来了之后才去处理,而不必一直阻塞地去等待事件,而且这样我们就不必开启一大堆的Socket!!不用开启一大堆的线程!!
最后附上游戏界面哈哈,java swing做的: