1. 程式人生 > >Android Studio實現一個PC和Android端的聊天室

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