最近在和老师做一个项目,要用到mina,今天做了个简单的多客户端点对点通信来熟悉一下这个框架,也遇到了几个问题,后面会说。
关于原理部分,我觉得这篇可以看看: https://www.cnblogs.com/duanxz/p/5143227.html。
开始吧。
服务器端
主要就是一个Handler和一个主类,这里放在一起了,注释挺详细的:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
public class Server {
private static final int PORT = 9123;
public static Map<String, IoSession> usersMap;
public static void main(String[] args){
// 监听连接的对象
IoAcceptor acceptor = new NioSocketAcceptor();
// 配置过滤器
// logger过滤器会输出所有的信息,例如新创建的会话、消息的接收、消息的发送、会话的关闭
// codec过滤器会转换二进制活协议规定的数据为消息对象,这里是处理基于文本的消息
acceptor.getFilterChain().addLast("logger", new LoggingFilter());
acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(
new TextLineCodecFactory(Charset.forName("UTF-8"))));
//
acceptor.setHandler(new ServerHandler());
// 设置输入缓冲区的大小和会话的IDLE熟悉
acceptor.getSessionConfig().setReadBufferSize(2048);
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
try {
acceptor.bind(new InetSocketAddress(PORT));//绑定端口,启动监听
usersMap=new HashMap<String, IoSession>();
System.out.println("HelloServer started on port " + PORT);
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ServerHandler extends IoHandlerAdapter {
//当有异常发生时触发
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
cause.printStackTrace();
session.close();
}
//收到来自客户端的消息
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
//来自客户端信息 from--to--message
String str = message.toString();
String info[] = str.split("--");
/*System.out.println("Message From " + info[0]);
System.out.println("Message To " + info[1]);*/
if(info[1].equals("0")){
Server.usersMap.put(info[0], session);
System.out.println("用户【"+info[0]+"】已登录");
}
else if(Server.usersMap.get(info[1])==null){
session.write("server : 对方未上线!");
}
else {
Server.usersMap.get(info[1]).write(info[0]+" : "+info[2]);
}
//接受客户端字符串"quit"关闭当前会话连接
if(str.trim().equalsIgnoreCase("quit")){
session.close();
}
}
//连接被关闭时触发
@Override
public void sessionClosed(IoSession session) throws Exception {
System.out.println("session closed from " + session.getRemoteAddress().toString());
Server.usersMap.remove((String) session.getAttribute("clientName"));
}
//有新连接时触发
@Override
public void sessionOpened(IoSession session) throws Exception {
System.out.println("Server sessionOpened "+System.currentTimeMillis());
System.out.println("session open for " + session.getRemoteAddress());
}
@Override
public void sessionCreated(IoSession session) throws Exception {
System.out.println("Server sessionCreated "+System.currentTimeMillis());
}
@Override
public void messageSent(IoSession session, Object message) throws Exception{
System.out.println("转发消息成功");
}
}
客户端
这里为了简便起见,规定了发送格式为“发送者–接受者–信息”
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.Scanner;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
public class Client {
public static void main(String[] args){
IoConnector connector = new NioSocketConnector();
connector.setConnectTimeoutMillis(30000);
connector.getFilterChain().addLast("logger", new LoggingFilter());
connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(
new TextLineCodecFactory(Charset.forName("UTF-8"))));
String name = "Mike";
connector.setHandler(new ClientHandler(name));
ConnectFuture future = connector.connect(new InetSocketAddress("localhost", 9123));
// 等待是否连接成功,相当于是转异步执行为同步执行。
future.awaitUninterruptibly();
// 连接成功后获取会话对象。如果没有上面的等待,由于connect()方法是异步的,session可能会无法获取。
IoSession session = future.getSession();
session.write(name+"--0--"+"login");//用于登录
System.out.println("Client session in main"+System.currentTimeMillis());
while(true){
Scanner scanner = new Scanner(System.in);
session.write(scanner.nextLine());
}
}
}
class ClientHandler extends IoHandlerAdapter {
private final String name;//用户名,用于标识这个用户,当然这里很粗略,并没有验证唯一性。
public ClientHandler(String name) {
this.name = name;
}
@Override
public void sessionCreated(IoSession session) throws Exception {
System.out.println("Client "+name+" sessionCreated "+System.currentTimeMillis());
}
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
String str = message.toString();
System.out.println(str);
}
@Override
public void sessionOpened(IoSession session) throws Exception {
System.out.println("Client "+name+" sessionOpened "+System.currentTimeMillis());
}
}
关于客户端这里,我有几个坑踩得挺久的:
- 过滤链中的编解码器要和服务器端的编解码一致,不然有可能会出现客户端收不到服务器端信息的情况。
- 我原本的思路是:客户端sessionCreated中放入属性name,然后服务器端sessionOpened中取出session中的name和session放到map中来表示用户已经登录。然后发现服务器端总是取不到这个name属性,后来通过看各个方法的执行时间戳,发现是服务器端的created和opened先执行,再执行客户端的created和opened,所以才换成在建立连接后先发个特殊的消息来表示登录。
聊天界面: