Android Studio實現一個PC和Android端的聊天室
最近想學一下關於Socket 通訊相關知識,所以果斷來個demo,一個很老套的東西,一個簡單的聊天室功能,服務端和android端可以一起聊天,好了不多說了,先上一個結構圖
圖是畫的有點點醜,但是還是能理解的哈,接下來就可以動手了,反正是做個demo不需要想太多,我們開啟AndroidStudio新建專案SocketDemo,工程建立完成後我們在專案下面建立一個javalib的module如下圖
名字隨便起,包名無所謂,我這裡新建了一個Test 的類包含main方法,類如下
package com.example; public class Test { public static void main(String[] args) { // TODO Auto-generated method stub } }
接下來我們把服務端的介面建立起來,這裡用到了java圖形開發的東西,我是一個做android的也不是很熟悉,百度了半天哈哈先上一個完成圖吧再來程式碼這樣直觀一點好解釋,
完成圖如下
服務端的介面就是這麼簡單,有了這個圖下面程式碼就很容易就看懂了,
public class Chatroom extends JFrame implements ActionListener { private JLabel clientLabel;//客戶列表標籤 private JList clientList;//客戶列表 private JLabel historyLabel;//聊天記錄標籤 private JScrollPane jScrollPane;//巢狀在聊天記錄外面的一個容器,讓裡面的內容可以滾動 private JTextArea historyContentLabel;//聊天記錄顯示的控制元件 private JTextField messageText;//服務端輸入框 private JButton sendButton;//服務端傳送的按鈕 public Chatroom() { clientLabel = new JLabel("客戶列表"); clientLabel.setBounds(0, 0, 100, 30); clientList = new JList<>(); clientList.setBounds(0, 30, 100, 270); historyLabel = new JLabel("聊天記錄"); historyLabel.setBounds(100, 0, 500, 30); historyContentLabel = new JTextArea(); jScrollPane=new JScrollPane(historyContentLabel); jScrollPane.setBounds(100, 30, 500, 230); //分別設定水平和垂直滾動條自動出現 jScrollPane.setHorizontalScrollBarPolicy( JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); jScrollPane.setVerticalScrollBarPolicy( JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); messageText = new JTextField(); messageText.setBounds(100, 270, 440, 30); sendButton = new JButton("傳送"); sendButton.setBounds(540, 270, 60, 30); //-----------程式碼分割線---------------- sendButton.addActionListener(this); this.setLayout(null); add(clientLabel); add(clientList); add(historyLabel); add(jScrollPane); add(messageText); add(sendButton); //設定窗體 this.setTitle("客服中心");//窗體標籤 this.setSize(600, 330);//窗體大小 this.setLocationRelativeTo(null);//在螢幕中間顯示(居中顯示) this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//退出關閉JFrame this.setVisible(true);//顯示窗體 this.setResizable(false); } @Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub if (e.getSource() == sendButton) { } }
首先是宣告一些控制元件,這個不用說很好理解,程式碼分割線下面的是設定事件回撥,我們只需要給button設定事件回撥就行,在下面一行是設定這個Jframe 的Layout,我這裡設定為null 的意思就是不需要任何佈局方式,我們利用位置來自己定位,再往下的一系列add不用說就是把宣告的控制元件新增到當前的JFrame裡面,再往下就是堆視窗的設定了,這些都不是重點,略過,到這裡我們服務端的介面就完成了.
接下來我們要實現的就是服務執行緒的程式碼了,服務端最核心的一個東西就是ServerSocket,我們通過serversocket迴圈監聽客戶端的連結,並且把已經連結的客戶端儲存起來就可以了,就是這麼簡單,先上程式碼
public class Server extends Thread {
boolean started = false;//標記服務是否已經啟動
ServerSocket ss = null;
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
try {
ss = new ServerSocket(8888);
started = true;
System.out.println("server is started");
} catch (BindException e) {
System.out.println("port is not available....");
System.out.println("please restart");
System.exit(0);
} catch (IOException e) {
e.printStackTrace();
}
try {
while (started) {
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
上面的就是server執行緒的一個整體的框架,開啟的是本地8888埠,後面我們慢慢加進去東西就行了,isStarted這個是迴圈的標記位,我們在前面的Chatroom類的構造最下面加入啟動server執行緒的程式碼跑起來看看
通過AS的控制檯我們看到這個服務已經啟動起來了,接下來我們就要監聽客戶端的到來了 ,這裡我們定義一個Client執行緒類,作為服務端對應的客戶,看程式碼
public class Client implements Runnable{
private Socket s;
private DataInputStream dis = null;
private DataOutputStream dos = null;
private boolean bConnected = false;
public Socket getSocket() {
return s;
}
public Client(Socket s) {
this.s=s;
try {
dis = new DataInputStream(s.getInputStream());
dos = new DataOutputStream(s.getOutputStream());
bConnected = true;
} catch (IOException e) {
e.printStackTrace();
}
}
public void send(String str) {
try {
dos.writeUTF(str);
} catch (IOException e) {
}
}
public void run() {
try {
while (bConnected) {
}
} catch (EOFException e) {
System.out.println("Client closed!");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (dis != null)
dis.close();
if (dos != null)
dos.close();
if (s != null) {
s.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Client client = (Client) o;
return s.equals(client.s);
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public String toString() {
// TODO Auto-generated method stub
return s.toString();
}
}
客戶端類最重要的一個東西就是Socket類,在這個Client類裡面加了get方法,toString,hash以及equals都是圍繞這個socket來的,因為它是這個類的大佬,client類的整體框架就是這樣了,另外對於訊息的接受我們放在while迴圈裡面,對於訊息的傳送我們呼叫socket 的writeUTF方法實現,客戶端的東西弄完了,我們現在回到server類裡面我們維護一個客戶端的列表
List<Client> clients = new ArrayList<Client>();
介於多個執行緒會訪問這個列表,並且ArrayList不是執行緒安全的,所以我們在Server類裡面建立幾個新增和刪除的方法
public synchronized void addClient(Client client) {
clients.add(client);
}
public synchronized void removeClient(Client client) {
clients.remove(client);
}
然後是server類的while方法加入監聽客戶端的程式碼
Socket s = ss.accept();
Client c = new Client(s, Server.this);
System.out.println("a client connected!");
new Thread(c).start();
addClient(c);
到這裡其實我們應該就能看到效果了只是現在我們android程式碼沒有寫,那我們簡單的寫一下Android的程式碼
先上佈局檔案
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="nanjing.jun.socketdemo.MainActivity">
<TextView
android:id="@+id/tv_service"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:background="#ddd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/et_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:id="@+id/btn_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="傳送" />
</LinearLayout>
</LinearLayout>
介面就是和QQ那樣上面是聊天的資訊下面是一個輸入框和一個傳送按鈕這裡介面我們先不管,先把Android端的client執行緒寫好
public class SocketThread extends Thread {
private Socket socket;
private boolean isConnected = false;
private DataInputStream dataInputStream;
private DataOutputStream dataOutputStream;
public SocketThread() {
}
public void disconnect() {
try {
dataInputStream.close();
dataOutputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
try {
// 建立一個Socket物件,並指定服務端的IP及埠號
socket = new Socket("10.137.213.28", 8888);
dataInputStream = new DataInputStream(socket.getInputStream());
dataOutputStream = new DataOutputStream(socket.getOutputStream());
System.out.println("~~~~~~~~連線成功~~~~~~~~!");
isConnected = true;
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
while (isConnected) {
try {
while (isConnected) {
String str = dataInputStream.readUTF();
if (str != null) {
}
}
} catch (EOFException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (dataInputStream != null)
dataInputStream.close();
if (dataOutputStream != null)
dataOutputStream.close();
if (socket != null) {
socket.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
public void sendMessage(String message) {
try {
dataOutputStream.writeUTF(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
這裡我的兩個手機和電腦都是在同一個區域網內的,電腦用的是8888埠,程式碼先就這樣,我們現在先把服務端程式碼跑起來,然後利用兩個手機跑一下APP,看下控制檯的輸出
到這裡為止,我們看到我們的兩個手機都能成功連線到服務端了對吧?其實已經成功了一大半了,接下來就是實現訊息的接受和傳送,我們來修改一下Server類,因為Server類要和服務端介面互動,這裡我採用回撥的方式通知服務介面客戶端的變化,訊息的變化,看下Server類加入介面後的程式碼
public class Server extends Thread {
public interface OnServiceListener {
void onClientChanged(List<Client> clients);
void onNewMessage(String message, Client client);
}
private OnServiceListener listener;
public void setOnServiceListener(OnServiceListener listener) {
this.listener = listener;
}
boolean started = false;
ServerSocket ss = null;
List<Client> clients = new ArrayList<Client>();
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
try {
ss = new ServerSocket(8888);
started = true;
System.out.println("server is started");
} catch (BindException e) {
System.out.println("port is not available....");
System.out.println("please restart");
System.exit(0);
} catch (IOException e) {
e.printStackTrace();
}
try {
while (started) {
Socket s = ss.accept();
Client c = new Client(s, Server.this);
System.out.println("a client connected!");
new Thread(c).start();
addClient(c);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public synchronized void newMessage(String msg, Client client) {
if (listener != null) {
listener.onNewMessage(msg, client);
}
}
public synchronized void addClient(Client client) {
clients.add(client);
if (listener != null) {
listener.onClientChanged(clients);
}
}
public synchronized void removeClient(Client client) {
clients.remove(client);
if (listener != null) {
listener.onClientChanged(clients);
}
}
}
然後我們在Chatroom類裡面進行回撥的註冊
看下修改後的程式碼
public class Chatroom extends JFrame implements Server.OnServiceListener, ActionListener {
private JLabel clientLabel;
private JList clientList;
private JLabel historyLabel;
private JScrollPane jScrollPane;
private JTextArea historyContentLabel;
private JTextField messageText;
private JButton sendButton;
private Server server;
private StringBuffer buffers;
public Chatroom() {
buffers = new StringBuffer();
clientLabel = new JLabel("客戶列表");
clientLabel.setBounds(0, 0, 100, 30);
clientList = new JList<>();
clientList.setBounds(0, 30, 100, 270);
historyLabel = new JLabel("聊天記錄");
historyLabel.setBounds(100, 0, 500, 30);
historyContentLabel = new JTextArea();
jScrollPane=new JScrollPane(historyContentLabel);
jScrollPane.setBounds(100, 30, 500, 230);
//分別設定水平和垂直滾動條自動出現
jScrollPane.setHorizontalScrollBarPolicy(
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
jScrollPane.setVerticalScrollBarPolicy(
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
messageText = new JTextField();
messageText.setBounds(100, 270, 440, 30);
sendButton = new JButton("傳送");
sendButton.setBounds(540, 270, 60, 30);
sendButton.addActionListener(this);
this.setLayout(null);
add(clientLabel);
add(clientList);
add(historyLabel);
add(jScrollPane);
add(messageText);
add(sendButton);
//設定窗體
this.setTitle("聊天室");//窗體標籤
this.setSize(600, 330);//窗體大小
this.setLocationRelativeTo(null);//在螢幕中間顯示(居中顯示)
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//退出關閉JFrame
this.setVisible(true);//顯示窗體
this.setResizable(false);
server = new Server();
server.setOnServiceListener(this);
server.start();
}
@Override
public void onClientChanged(List<Client> clients) {
// TODO Auto-generated method stub
clientList.setListData(clients.toArray());
}
@Override
public void onNewMessage(String message, Client client) {
// TODO Auto-generated method stub
buffers.append(client.getSocket().getInetAddress().toString()+"\n");
buffers.append(message+"\n");
historyContentLabel.setText(buffers.toString());
}
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
if (e.getSource() == sendButton) {
Client client = (Client) clientList.getSelectedValue();
client.send(messageText.getText().toString());
buffers.append("伺服器"+"\n");
buffers.append(messageText.getText().toString()+"\n");
}
}
}
我們再看看服務端Client修改的程式碼
public class Client implements Runnable{
private Socket s;
private DataInputStream dis = null;
private DataOutputStream dos = null;
private boolean bConnected = false;
private Server server;
public Socket getSocket() {
return s;
}
public Client(Socket s, Server ser) {
this.s=s;
this.server = ser;
try {
dis = new DataInputStream(s.getInputStream());
dos = new DataOutputStream(s.getOutputStream());
bConnected = true;
} catch (IOException e) {
e.printStackTrace();
}
}
public void send(String str) {
try {
dos.writeUTF(str);
} catch (IOException e) {
server.removeClient(this);
}
}
public void run() {
try {
while (bConnected) {
String str = dis.readUTF();
server.newMessage(str,this);
}
} catch (EOFException e) {
System.out.println("Client closed!");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (dis != null)
dis.close();
if (dos != null)
dos.close();
if (s != null) {
server.removeClient(this);
s.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Client client = (Client) o;
return s.equals(client.s);
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public String toString() {
// TODO Auto-generated method stub
return s.toString();
}
}
我們先看看Client程式碼,這裡我把Server傳了進來,在Client接收到訊息和異常退出的時候我們通過Server例項來呼叫對應的Server裡面的方法,再回看Server裡面,我們的訊息接受和Client退出已經新的Client 的到來我們都通過回撥的方式通知服務端的介面ChatRoom類,到這裡服務端幾個部分的通訊基本是完成了,接下來我們完善android端的程式碼主要是實現訊息的傳送和接受,這裡我們同樣以回撥的方式來實現,看下android端Client 的實現
public class SocketThread extends Thread {
public interface OnClientListener {
void onNewMessage(String msg);
}
private OnClientListener onClientListener;
public void setOnClientListener(OnClientListener onClientListener) {
this.onClientListener = onClientListener;
}
private Socket socket;
private boolean isConnected = false;
private DataInputStream dataInputStream;
private DataOutputStream dataOutputStream;
public SocketThread(OnClientListener onClientListener) {
this.onClientListener = onClientListener;
}
public void disconnect() {
try {
dataInputStream.close();
dataOutputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
try {
// 建立一個Socket物件,並指定服務端的IP及埠號
socket = new Socket("10.137.213.28", 8888);
dataInputStream = new DataInputStream(socket.getInputStream());
dataOutputStream = new DataOutputStream(socket.getOutputStream());
System.out.println("~~~~~~~~連線成功~~~~~~~~!");
isConnected = true;
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
while (isConnected) {
try {
while (isConnected) {
String str = dataInputStream.readUTF();
if (str != null) {
if (onClientListener != null) {
onClientListener.onNewMessage(str);
}
}
}
} catch (EOFException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (dataInputStream != null)
dataInputStream.close();
if (dataOutputStream != null)
dataOutputStream.close();
if (socket != null) {
socket.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
public void sendMessage(String message) {
try {
dataOutputStream.writeUTF(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
看下MainActivity 的實現
public class MainActivity extends AppCompatActivity implements SocketThread.OnClientListener{
private SocketThread socketThread;
private StringBuilder stringBuilder=new StringBuilder();
private TextView serviceTv;
private EditText contentEt;
private Button sendBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
serviceTv = (TextView) findViewById(R.id.tv_service);
contentEt = (EditText) findViewById(R.id.et_content);
sendBtn = (Button) findViewById(R.id.btn_send);
socketThread = new SocketThread(this);
socketThread.start();
sendBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
stringBuilder.append("我:\n");
stringBuilder.append(contentEt.getText().toString());
stringBuilder.append("\n");
serviceTv.setText(stringBuilder.toString());
socketThread.sendMessage(contentEt.getText().toString());
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
socketThread.disconnect();
}
@Override
public void onNewMessage(String msg) {
stringBuilder.append("伺服器:");
stringBuilder.append("\n");
stringBuilder.append(msg);
stringBuilder.append("\n");
runOnUiThread(new Runnable() {
@Override
public void run() {
serviceTv.setText(stringBuilder.toString());
}
});
}
}
通過一個StringBuffer來達到訊息記錄的功能,程式碼就是這麼簡單,我們看下跑起來的效果
到這裡我們實現了手機向伺服器傳送資訊,伺服器可以向指定的手機發送資訊,這裡我在Chatroom類裡面實現的是通過點選選中左邊的客戶來進行訊息的傳送,我們可以看到基本的樣子就是這樣了,接下來要實現的就是一個手機發送的資訊在另外一個手機能看到,這就需要伺服器來轉發訊息了,這裡需要一個小小的協議就是客戶端要知道訊息是來自誰的
所以我們在伺服器轉發或者傳送資訊的時候前面加上誰傳送的,這裡我們用一個$符號隔開,在android端收到資訊的時候拆開就行了,我們修改一下Server類接收到訊息的方法,然後新增一個傳送訊息的方法給Chatroom呼叫
public synchronized void snedMessage(String msg) {
for (Client client1 : clients) {
client1.send(msg);
}
}
public synchronized void newMessage(String msg, Client client) {
if (listener != null) {
listener.onNewMessage(msg, client);
for (Client client1 : clients) {
if (!client1.equals(client)) {
client1.send(client1.getSocket().getInetAddress() + "#" + msg);
}
}
}
}
ChatRoom類裡面按鈕的點選事件修改為
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
if (e.getSource() == sendButton) {
server.snedMessage("伺服器#"+messageText.getText().toString());
buffers.append("伺服器"+"\n");
buffers.append(messageText.getText().toString()+"\n");
historyContentLabel.setText(buffers.toString());
}
}
android端接受到訊息的處理
@Override
public void onNewMessage(String msg) {
Log.i("收到的資訊i",msg);
String[] s = msg.split("#");
stringBuilder.append(s[0]);
stringBuilder.append("\n");
stringBuilder.append(s[1]);
stringBuilder.append("\n");
runOnUiThread(new Runnable() {
@Override
public void run() {
serviceTv.setText(stringBuilder.toString());
}
});
}
看看實現後的效果圖
到這裡就大功告成了,通過Server類的轉發我們後面還可以進行點對點通訊,通過自定義協議我們可以完成各種各樣的業務,自己動手實現一個及時通訊的框架就可以這樣完成了,是不是很簡單,程式碼我上傳到github
https://github.com/wlj644920158/SocketDemo