Java实现轻量级IM(客户端)

October 14, 2018 3 min read Author: Yu

上文以及以后也将会用到的代码块插件叫Highlight.js,支持的语言以及风格也比较多,还可以自己写css调整样式,算是一款不错的国产插件

github:WP Code Highlight.js

编者注:

该文章由旧博客系统迁移而来,上述插件为wordpress相关插件且已停止维护。

上篇已经详细拆解了服务端的代码,总体来说比较简单,服务器要做的是连接、接收、处理、发送。在上文的例子中为了方便测试处理方式比较简单,就是收到任意客户端发来的信息时直接广播给其他所有的客户端。本文就来解析客户端的实现以及应用。

纵观服务端的实现,客户端的实现已经非常明了:1、开启线程被动接收数据->处理数据 2、实现一个开启线程发送数据的方法 因为要开启线程处理数据,所以客户端依然要继承Thread,且基本数据有一下几个:

private String host;
private int port;
private ObjectInputStream in;
private ObjectOutputStream out;
private Socket socket;

然后实现构造方法:

public ConnectClient(String host, int port) {
this.host = host;
this.port = port;
try {
socket = new Socket(host, port);
out = new ObjectOutputStream(socket.getOutputStream());
in = new ObjectInputStream(socket.getInputStream());
} catch (IOException e) {
}
}

这里需要注意上文提到IO流的初始化顺序的问题,因为服务端是先I后O,所以这里先O后I。

后面的实现非常简单,接收信息的方法写在run中,然后写一个发送方法:

public void run() {
try {
while (true) {
Object msg = in.readObject();
//1
}
} catch (IOException | ClassNotFoundException e) {
//2
}
}
public void sendData(TalkPkg tp) {
new Thread(() -> {
try {
out.writeObject(tp);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}

那么问题来了,接收到的消息如何处理呢?考虑到客户端的代码要放在Android上跑,所以比较稳妥的做法是给各个情况分别写一个监听事件,这里写两个事件作为示范,分别在run方法中的接收信息的注释1和注释2处。显然,注释1的地方应该是接收到了信息,等待处理;第二个是则是输入流发生异常(一般是连接中断)。所以我们定义两个内部接口,分别为onReceivedListener和onConnectLostListener来处理上述两种情况,代码如下:

public interface onReceivedListener {
void onReceived(Object msg);
}
public interface onConnectLostListener {
void onConnectLost();
}

并且在类中声明该接口:

private onReceivedListener receivedListener;
private onConnectLostListener connectLostListener;

set方法:

public void setReceivedListener(onReceivedListener on) {
this.receivedListener = on;
}
public void setConnectLostListener(onConnectLostListener connectLostListener) {
this.connectLostListener = connectLostListener;
}

在相关位置调用:

public void run() {
try {
while (true) {
Object msg = in.readObject();
receivedListener.onReceived(msg);
}
} catch (IOException | ClassNotFoundException e) {
connectLostListener.onConnectLost();
}
}

至此,客户端的主要代码就完成了,先贴出完整代码:

public class ConnectClient extends Thread {
private String host;
private int port;
private ObjectInputStream in;
private ObjectOutputStream out;
private Socket socket;
private onReceivedListener receivedListener;
private onConnectLostListener connectLostListener;
public ConnectClient(String host, int port) {
this.host = host;
this.port = port;
try {
socket = new Socket(host, port);
out = new ObjectOutputStream(socket.getOutputStream());
in = new ObjectInputStream(socket.getInputStream());
} catch (IOException e) {
}
}
public interface onReceivedListener {
void onReceived(Object msg);
}
public interface onConnectLostListener {
void onConnectLost();
}
public void setReceivedListener(onReceivedListener on) {
this.receivedListener = on;
}
public void setConnectLostListener(onConnectLostListener connectLostListener) {
this.connectLostListener = connectLostListener;
}
@Override
public void run() {
try {
while (true) {
Object msg = in.readObject();
receivedListener.onReceived(msg);
}
} catch (IOException | ClassNotFoundException e) {
connectLostListener.onConnectLost();
}
}
public void sendData(TalkPkg tp) {
new Thread(() -> {
try {
out.writeObject(tp);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}

我们使用实际环境来测试客户端及服务端,客户端当然万年Android,服务端ubuntu+jre10server版,不过在测试之前还有最后一个小问题,就是用来进行数据传输的类必须实现Serializable接口,并定义一个serialVersionUID,如果没有定义serialVersionUID,java会自动生成一个serialVersionUID来使用,这个序列化版本ID的作用是确定类的版本一致,需要注意的是,如果不自己定义serialVersionUID而让java自动生成,那么需要保证类的结构一模一样,并且类所处的包的相对路径也一模一样,java自动生成的版本ID才能成功的进行序列化和反序列化,才能在IO流中传输。这里我们传输的类比较简单,所以不定义serialVersionUID而是让java去自动生成。以下为该类的代码:

package server.myconnect;
import java.io.Serializable;
public class TalkPkg implements Serializable {
private int avatar_id;
private String message;
public TalkPkg(int avatar_id, String message) {
this.avatar_id = avatar_id;
this.message = message;
}
public int getAvtar_id() {
return avatar_id;
}
public String getMessage() {
return message;
}
}

传输的数据也简单,就一个头像id和信息,下面我们在客户端中使用ConnectClient来完成数据传输。基本界面用recyclerview+adapter实现,这里不谈。下面是主要代码:

new Thread(() -> {
connectClient=new ConnectClient("SERVER_ADDRESS",8000);
connectClient.start();
connectClient.setReceivedListener(new ConnectClient.onReceivedListener() {
@Override
public void onReceived (Object msg){
TalkPkg res = (TalkPkg) msg;
chat_list.add(new ChatItem(false, res.getAvtar_id(), res.getMessage()));
runOnUiThread(new Runnable() {
@Override
public void run() {
chatAdapter.notifyItemInserted(chat_list.size());
}
});
}
});
}).start();
send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick (View v){
connectClient.sendData(new TalkPkg(1, input.getText().toString()));
chat_list.add(new ChatItem(true, 1, input.getText().toString()));
chatAdapter.notifyItemInserted(chat_list.size());
Toast.makeText(MainActivity.this, "sended", Toast.LENGTH_SHORT).show();
}
});

ConnectClient的构造方法中有阻塞方法,所以初始化过程需要放在线程中执行,而后为此设置一个监听事件,当接收到信息时,将信息提取并且显示在列表中。下面的点击事件也比较简单,为发送按钮设置监听事件,发送信息、添加进列表等,下面来测试实际效果,用一台ubuntu服务器来部署服务端,用刚才说到的Android8.0作为客户端,用Intellij的控制台充当另一个客户端。服务端main方法中的代码:

ConnectServer cs=new ConnectServer(8000);
cs.start();

另一个客户端的代码也是几句(这里略掉了服务器IP):

ConnectClient cc=new ConnectClient("SERVER_ADDRESS",8000);
Scanner in=new Scanner(System.in);
String t;
while (true){
System.out.println("pls input:");
t=in.next();
cc.sendData(new TalkPkg(1, t));
}