Android网络聊天室实现过程中遇到的问题及解决
服务器端
- 思路
- 创建服务器,绑定端口号
- while(true)无限循环不断接收客户端的连接请求
- 将各个客户端对应的socket添加到集合,方便统一管理
- 为各个客户端的socket开启子线程,实现通信(接收,转发)
- 结构
在eclipse中创建两个类:Server—对应服务器;ServerThread—对应子线程,重写run()方法,实现接收和转发
源码
- Server.java
public class Server {
private static List<Socket> socket_list = new LinkedList<>();
public static void main(String[] args) {
try (
ServerSocket server = new ServerSocket(8888);
) {
//使用try with resources确保出现异常时服务器的关闭
try {
String hostAddress = InetAddress.getLocalHost().getHostAddress();
System.out.println("服务器 " + hostAddress + " 已就绪");
while (true) {
Socket socket = server.accept();
//接收客户端的请求
socket_list.add(socket);
//添加到集合
System.out.println("ip: " + socket.getInetAddress().getHostAddress() + " 加入聊天室");
new Thread(new ServerThread(socket, socket_list)).start();
//开启线程
}
} catch (UnknownHostException e) {
System.out.println("找不到主机名称");
e.printStackTrace();
}
} catch (IOException e) {
System.out.println("服务器端口已被占用,无法建立服务器");
e.printStackTrace();
}
}
}
- ServerThread.java
public class ServerThread implements Runnable {
private List<Socket> socket_list = new LinkedList<>();
private Socket mSocket = null;
public ServerThread(Socket socket, List<Socket> list) {
this.mSocket = socket;
socket_list = list;
//拿到socket的集合,为转发做基础
}
@Override
public void run() {
//try with resources确保流的关闭
try (
BufferedReader br = new BufferedReader(
new InputStreamReader(mSocket.getInputStream()));
//读取文本信息,考虑可以用BufferedReader的readLine()方法读取到换行符
PrintStream ps = new PrintStream(
new FileOutputStream("chatting records", true));
//写出到服务器的文件,做记录
//println()可以直接写出换行符
) {
String content = null;
while(true) {
while ((content = br.readLine()) != null) {
ps.println(content);
System.out.println(content);
Iterator<Socket> it = socket_list.iterator();
//使用迭代器,获得每一个socket对象
while (it.hasNext()) {
Socket socket = it.next();
if (socket != mSocket) {
//自己不需要接收自己的消息
try {
PrintStream tps = new PrintStream(socket.getOutputStream());
tps.println(content);
} catch (IOException e) {
//创建流对象失败,意味着此socket已关闭,客户端已下线,直接在集合中移除
it.remove();
}
}
}
}
}
} catch (IOException e) {
System.out.println("客户端已断开连接");
socket_list.remove(mSocket);
System.out.println("ip: " + mSocket.getInetAddress().getHostAddress() + " 退出了聊天室");
}
}
}
重点内容
- 客户端之间的通信
服务端接收客户端消息,向客户端发送消息我们在Java网络编程已经有了相关基础.那么我们是怎么实现客户端之间的通信的呢?
我们可以在服务端创建一个LinkedList集合,将所有与客户端相连接的socket保存起来,在接收到一个socket发送过来的信息后,使用迭代器逐个获取所有的socket对象,一一向每一个socket发送信息,这样就实现了客户端之间的通信
客户端
- 思路
- 登录页面,输入用户名,与服务器建立连接
- 聊天页面,接收信息,发送信息,UI更新等操作
- 考虑到多个页面都会用到socket,BufferedReader等,创建BaseActivity方便变量访问
BaseActivity内容
public class BaseActivity extends AppCompatActivity {
public static Socket socket;
public static BufferedReader br;
public static BufferedWriter bw;
private NotNetworkReceiver receiver;
public void notNetworkBroadcast(){
Intent intent = new Intent("com.example.chatroom.NOTNETWORK");
sendBroadcast(intent);
//发送广播
}
@Override
protected void onResume() {
super.onResume();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("com.example.chatroom.NOTNETWORK");
receiver = new NotNetworkReceiver();
registerReceiver(receiver, intentFilter);
//动态注册广播
}
@Override
protected void onPause() {
super.onPause();
if(receiver != null){
unregisterReceiver(receiver);
receiver = null;
}
//注销广播
}
private class NotNetworkReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "无连接,请检查您的网络", Toast.LENGTH_SHORT).show();
}
}
}
- 重点内容
- 权限申请
要与服务器建立连接,需要首先在Manifest获取权限,
在Manifest中添加
<uses-permission android:name="android.permission.INTERNET" />
- 与服务器建立连接
new Thread(new Runnable() {
@Override
public void run() {
try {
BaseActivity.socket = new Socket("192.168.1.126", 8888);
//a.建立与服务器连接
BaseActivity.br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BaseActivity.bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
ChatActivity.actionStart(LoginActivity.this, userName);
finish();
} catch (IOException e) {
notNetworkBroadcast();
//b.发送广播,通过广播接收器显示Toast
e.printStackTrace();
}
}
a. 在Android中,Socket socket = new Socket();需要放在子线程中进行,所以为其开启线程.
b. 与socket不同,Toast只能在主线程中显示,因此用广播来实现Toas显示
3. UI更新
与Toast相同,Ui的更新操作也只能在主线程中操作,但是我们客户端的接收和发送信息肯定是会开启子线程的,那么该如何进行编写?
有两种方法:1.使用Handle完成UI更新;
2.使用runOnUiThread()完成更新,这里我使用的第二种
runOnUiThread(new Runnable() {
@Override
public void run() {
adapter.notifyDataSetChanged();
//消息更新
msgRecycleView.scrollToPosition(msgList.size() - 1);
//显示最后一行,即最新消息
}
});
对了,在更新UI之前,记得要将消息列表进行更新,就像这样.
msgList.add(new Msg(content, Msg.TYPE_RECIVED));