作者:鄒峰立莲镣,微博:zrunker福稳,郵箱:zrunker@yahoo.com,微信公眾號:書客創(chuàng)作瑞侮,個人平臺:www.ibooker.cc的圆。
本文選自書客創(chuàng)作平臺第31篇文章。閱讀原文 区岗。
簡易聊天室,什么是聊天室呢毁枯,簡單一點說就是一些人可以共同聊天慈缔,別人能夠看見你發(fā)布的消息,你也可以看到別人的消息种玛,大家的消息是公開的藐鹤。
功能分析:
1、聊天功能赂韵,聊天是一個長時間的相互交互的過程娱节,要實現(xiàn)長時間連接Socket是一個比較不錯的選擇。
2祭示、一些人相互聊天功能肄满,要想實現(xiàn)相互聊天,就要將消息轉(zhuǎn)發(fā)給所有建立連接的人,這里就要進(jìn)行消息轉(zhuǎn)發(fā)稠歉。
實現(xiàn)思路:
利用Socket實現(xiàn)客戶端和服務(wù)端長連接掰担,每次將連接進(jìn)來的Socket保存到一個集合當(dāng)中,當(dāng)其中一個Socket有接收到數(shù)據(jù)的時候怒炸,將接收到的數(shù)據(jù)轉(zhuǎn)發(fā)給其他的Socket带饱,這樣既能實現(xiàn)聊天功能又能實現(xiàn)轉(zhuǎn)發(fā)。
對Socket的操作應(yīng)放在子線程當(dāng)中阅羹,每一次有新的Socket連接進(jìn)來之后就新開一個子線程勺疼。
如果對Socket不是很了解,推薦【JavaEE】Socket簡單體驗, TCP和UDP這篇文章進(jìn)行了解捏鱼。
服務(wù)端搭建
首先要實現(xiàn)監(jiān)聽客戶端連接执庐,需要利用ServerSocket的accept方法進(jìn)行監(jiān)聽,但是該方法會將程序阻塞穷躁,所以該監(jiān)聽過程要放在子線程中完成耕肩。這里定義的線程為ServerListener。
其次每監(jiān)聽到一個Socket问潭,Socket無論是獲取還是傳輸過程都是對字節(jié)流進(jìn)行操作猿诸,這是一個比較耗時的操作,所以也要放在子線程中操作狡忙。這里定義的線程為ChatSocket梳虽。
最后還要定義一個Socket的子線程管理類,方便對所有連接進(jìn)來的Socket的子線程進(jìn)行管理灾茁,例如對數(shù)據(jù)的轉(zhuǎn)發(fā)窜觉。這里定義的管理類為ChatManager。
在程序最開始運行的時候北专,要開啟對Socket連接的監(jiān)聽禀挫,所以在main方法中開始監(jiān)聽線程。
public static void main(String[] args) {
// 開啟服務(wù)端監(jiān)聽
new ServerListener().start();
}
開啟監(jiān)聽線程之后拓颓,當(dāng)監(jiān)聽到有Socket進(jìn)行連接的時候语婴,不僅要新建一個子線程實現(xiàn)該Socket的操作,而且要把該Socket添加到Socket線程管理類ChatManager當(dāng)中驶睦,方便后期管理砰左。
public class ServerListener extends Thread {
@Override
public void run() {
try {
// 1、創(chuàng)建ServerScoket场航,設(shè)置端口
ServerSocket serverSocket = new ServerSocket(12345);
while (true) {
// 2缠导、accept方法將導(dǎo)致程序阻塞
Socket socket = serverSocket.accept();
JOptionPane.showMessageDialog(null, "有客戶端連接到本機(jī)的12345端口");
// 3、將socket傳遞給新線程
ChatSocket cs = new ChatSocket(socket);
cs.start();
// 4溉痢、使用Chatmanager進(jìn)行管理
ChatManager.getInstance().add(cs);
}
} catch (IOException e) {e.printStackTrace();}
}
}
在ChatSocket新線程中僻造,要實現(xiàn)對接收到的Socket進(jìn)行處理憋他,例如獲取Socket客戶端發(fā)送過來的數(shù)據(jù)進(jìn)行轉(zhuǎn)發(fā)等。而轉(zhuǎn)發(fā)過程嫡意,是轉(zhuǎn)發(fā)給所有連接進(jìn)來的Socket举瑰,所以轉(zhuǎn)發(fā)過程的實現(xiàn)將由ChatManager完成。
ChatSocket在一開啟線程的時候蔬螟,就要進(jìn)行客戶端數(shù)據(jù)獲取此迅,并轉(zhuǎn)發(fā)。
ChatSocket要能夠完成服務(wù)端想客戶端發(fā)送數(shù)據(jù)的過程旧巾。
public class ChatSocket extends Thread {
private Socket socket;
public ChatSocket(Socket socket) {
this.socket = socket;
}
// 服務(wù)端傳值給客戶端
public void out(String out) {
try {
if (socket.isConnected() && !socket.isClosed()) {
// 獲取當(dāng)前Socket輸出流耸序,輸出數(shù)據(jù)
socket.getOutputStream().write(out.getBytes("gbk"));
System.out.println("轉(zhuǎn)發(fā)數(shù)據(jù)**********" + out);
} else {
// 鏈接已關(guān)閉
ChatManager.getInstance().remove(this);
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
if (socket.isConnected() && !socket.isClosed()) {
// 接受客戶端數(shù)據(jù)
BufferedReader br = new BufferedReader(new InputStreamReader(
socket.getInputStream(), "gbk"));
// 讀取數(shù)據(jù)
String line = null;
while ((line = br.readLine()) != null) {
// 轉(zhuǎn)發(fā)數(shù)據(jù)
ChatManager.getInstance().publish(this, line);
System.out.println("接受數(shù)據(jù)******" + line);
}
br.close();
} else {
// 鏈接已關(guān)閉
ChatManager.getInstance().remove(this);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
最后來說說Socket線程管理類ChatManager,一個聊天室只能有一個Socket線程管理類鲁猩,所以該類要使用單例模式坎怪。
private static ChatManager cm;
// 單例
public static ChatManager getInstance() {
if (cm == null) {
cm = new ChatManager();
}
return cm;
}
提供Socket子線程存放集合,能夠?qū)崿F(xiàn)Socket子線程添加和移除功能廓握。
// ChatSocket集合
Vector vector = new Vector<>();
// 添加ChatSocket
public void add(ChatSocket chatSocket) {
vector.add(chatSocket);
}
// 移除ChatSocket
public void remove(ChatSocket chatSocket) {
vector.remove(chatSocket);
}
最后是借助Socket子線程存放集合搅窿,實現(xiàn)數(shù)據(jù)轉(zhuǎn)發(fā)功能。
// 發(fā)送消息
public void publish(ChatSocket cs, String out) {
for (int i = 0; i < vector.size(); i++) {
ChatSocket chatSocket = vector.get(i);
if (!cs.equals(chatSocket)) {
chatSocket.out(out);
}
}
}
Android端搭建
對于Android客戶端而言就顯得稍微簡單一些隙券,只需要實現(xiàn)數(shù)據(jù)的接受和傳遞即可男应。
首先是界面的構(gòu)建:
客戶端界面
當(dāng)點擊連接的時候,客戶端會向服務(wù)端發(fā)送連接請求娱仔,請求成功之后便可發(fā)送消息沐飘,聊天內(nèi)容將會在界面中間部分顯示。
1牲迫、初始化信息耐朴,在Activity中完成對控件的初始化和點擊事件監(jiān)聽。
// 定義控件全局變量
private TextView ipTv, contentTv;
private Button linkBtn, sendBtn;
private EditText sendEd;
// 初始化控件
private void initView() {
ipTv = (TextView) findViewById(R.id.tv_ip);
contentTv = (TextView) findViewById(R.id.tv_content);
linkBtn = (Button) findViewById(R.id.btn_link);
linkBtn.setOnClickListener(this);
sendBtn = (Button) findViewById(R.id.btn_send);
sendBtn.setOnClickListener(this);
sendEd = (EditText) findViewById(R.id.ed_send);
}
// 點擊事件監(jiān)聽
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_link:// 鏈接
connect();
break;
case R.id.btn_send:// 發(fā)送
send();
break;
}
}
2盹憎、連接服務(wù)端筛峭,并獲取服務(wù)端傳遞數(shù)據(jù)。
// 定義三個全局變量
private Socket socket;
private BufferedReader reader;
private BufferedWriter writer;
// 連接Socket服務(wù)端
private void connect() {
final String ip = ipTv.getText().toString().trim();
final int port = 12345;
// 異步執(zhí)行
AsyncTask asyncTask = new AsyncTask() {
@Override
protected Void doInBackground(Void... params) {
try {
// 實例化Socket
socket = new Socket(ip, port);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
publishProgress("success");
// 讀取傳遞過來的數(shù)據(jù)
String line;
while ((line = reader.readLine()) != null) {
publishProgress(line);
}
} catch (IOException e) {
publishProgress("failed");
e.printStackTrace();
}
return null;
}
@Override
protected void onProgressUpdate(String... values) {
if ("success".equals(values[0]))
Toast.makeText(MainActivity.this, "連接成功", Toast.LENGTH_LONG).show();
else if ("failed".equals(values[0]))
Toast.makeText(MainActivity.this, "無法建立連接", Toast.LENGTH_LONG).show();
else
contentTv.append("他說:" + values[0] + "\n");
super.onProgressUpdate(values);
}
};
asyncTask.execute();
}
注意連接服務(wù)端和都去服務(wù)端數(shù)據(jù)是一個耗時的操作陪每,所以應(yīng)放在子線程中完成影晓,這里是采用異步AsyncTask的方式來進(jìn)行處理。
3奶稠、發(fā)送數(shù)據(jù)
// 向Socket服務(wù)端發(fā)送
private void send() {
String out = sendEd.getText().toString().trim();
// 發(fā)送數(shù)據(jù)
try {
writer.write(out + "\n");
writer.flush();
sendEd.setText("");
contentTv.append("我說:" + out + "\n");
} catch (IOException e) {
e.printStackTrace();
}
}
到這里就已經(jīng)全部完成了俯艰,包括服務(wù)端和Android客戶端的實現(xiàn)捡遍,在實際開發(fā)當(dāng)中往往是通過構(gòu)建一些比較成熟的框架來實現(xiàn)這一過程锌订,實現(xiàn)原理大致相同,實現(xiàn)過程有所不同画株。
Github后臺代碼地址
Github安卓客戶端代碼地址
閱讀原文