首先制作一個客戶端,界面如下:
使用方法:啟動后,首先在登錄編輯框輸入一個昵稱,然后點擊登錄,上面灰色區域是聊天窗,其中會顯示你的登錄提示,顯示其他人發的消息。在的登錄成功后,可以在下面的發送編輯框內編輯你要發的信息,點擊發送就可以推送給當前所有登錄中的用戶,下線的方法就是發送“bye”,之后便不會再接收到其他人的信息。
代碼如下:
布局的代碼:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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" tools:context=".MainActivity"> <TextView android:id="@+id/msg" android:layout_width="405dp" android:layout_height="355dp" android:layout_marginStart="23dp" android:layout_marginEnd="23dp" android:background="#DDDDDD" android:maxLines="600" android:scrollbars="vertical" app:layout_constraintBottom_toTopOf="@+id/log" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.514" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.133" /> <EditText android:id="@+id/chat" android:layout_width="153dp" android:layout_height="52dp" android:layout_marginEnd="44dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/send" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.867" /> <EditText android:id="@+id/name" android:layout_width="153dp" android:layout_height="52dp" android:layout_marginEnd="44dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.846" app:layout_constraintStart_toEndOf="@+id/log" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.754" /> <Button android:id="@+id/log" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="58dp" android:layout_marginEnd="66dp" android:text="登錄" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/name" app:layout_constraintHorizontal_bias="0.62" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.75" /> <Button android:id="@+id/send" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="62dp" android:layout_marginEnd="62dp" android:text="發送" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/chat" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.881" /> </androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
package com.example.chatroom; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.os.StrictMode; import android.text.method.ScrollingMovementMethod; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; public class MainActivity extends AppCompatActivity { private Button send=null; private Button log=null; private String s=""; private EditText name=null; private EditText chat=null; private TextView msg=null; private PrintStream out=null; private BufferedReader msgget=null; private Socket client=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //下面這兩句是為了使程序能夠在主線程中創建Socket StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads().detectDiskWrites().detectNetwork() .penaltyLog().build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects().penaltyLog().penaltyDeath() .build()); name=(EditText)findViewById(R.id.name); send=(Button)findViewById(R.id.send); log=(Button)findViewById(R.id.log); chat=(EditText)findViewById(R.id.chat) ; msg=(TextView)findViewById(R.id.msg); msg.setMovementMethod(new ScrollingMovementMethod()); log.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { star(); } }); send.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //發送信息到服務器 out.println(chat.getText().toString()); chat.setText(""); } }); } //此函數的作用是連接至服務器並開啟一個線程來接收服務器發來的消息 public void star(){ try { client=new Socket("10.0.2.2",9090); msg.append("已連接至服務器\n"); msg.append("已加入聊天 "+"當前身份: "+name.getText().toString()+" \n"); out=new PrintStream(client.getOutputStream()); msgget=new BufferedReader(new InputStreamReader(client.getInputStream(),"UTF-8")); out.println(name.getText().toString()); //這里啟動一個新線程來不斷監聽服務器發來的消息,直到收到“bye”為止 new Thread() { public void run(){ try { while(true){ s=msgget.readLine(); if(s!=""&&s!=null){ if(s.equals("bye")){ msg.append("已退出聊天"); break;} msg.append(s+"\n"); } } } catch (Exception e) { e.printStackTrace(); } } }.start(); } catch (Exception e) { e.printStackTrace(); System.out.println("連接失敗"); } } }
客戶端邏輯還是很簡單的:連接服務器並接收服務器發來的消息,最后把服務器的消息顯示在Textview中。
接下來是服務器
服務器是這個項目的難點所在,先梳理一下思路:首先是要利用線程,為每一個用戶的連接創建一個新線程,這些線程一對一的去接收來自用戶的信息,然后將受到的信息發送到所有客戶端,在接收到某用戶的“bye”則斷開與該用戶的連接結束他的線程。
如何管理這些Socket連接就是難點,我使用的方法是創建一個管理線程的工具類,在其中定義好靜態方法以及靜態變量,我定義了一個List<Socket> serverlist來存放各個線程的連接,然后在需要把某用戶的消息廣播時,就遍歷 serverlist來把消息發送到所有的用戶。在用戶下線時,在從中匹配(Socket支持使用"=="進行比較)找到該用戶的Socket連接把它關閉並從serverlist中移除。
工具類代碼如下:
import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class socketlist { static List<Socket> serverlist=new ArrayList<Socket>(); //添加新連接 static void add(Socket client) { serverlist.add(client); } //廣播消息 static void sendall(String s) throws UnsupportedEncodingException, IOException { PrintWriter out = null; for (Socket client : serverlist) { out=new PrintWriter(new OutputStreamWriter(client.getOutputStream(),"UTF-8"),true); out.println(s); } } //移除目標連接 static void dele(Socket client) throws IOException { for(int i=0;i<serverlist.size();i++) { if(client==serverlist.get(i)) { serverlist.get(i).close(); serverlist.remove(i); } } } }
接着是線程類
客戶端登錄時會首先發送自己的昵稱到服務器,因此在服務器中就把收的到的第一條信息作為用戶的昵稱,之后便不斷監聽是相應的用戶否有消息發來,有的話就通過工具類廣播給所有的用戶,收到“bye”就使用工具類移除自己的連接並結束線程。
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.Socket; public class netthread extends Thread { private Socket client; private String name; int i=0; public netthread(Socket socket) { this.client=socket; } public void run() { BufferedReader msg = null; PrintWriter out = null; String s; try { out=new PrintWriter(new OutputStreamWriter(client.getOutputStream(),"UTF-8"),true); msg=new BufferedReader(new InputStreamReader(client.getInputStream(),"UTF-8"));//對緩沖區數據讀取 StringBuffer info=new StringBuffer(); info.append(msg.readLine());//接收的數據 s=info.toString(); while(!s.equals("bye")) { if(!s.equals("")&&!s.equals("null")){ //判斷是不是第一條消息,是的話就作為昵稱 if(i==0) {name=s;i=1; socketlist.sendall(name+" 加入聊天室"); System.out.println(name+" 加入聊天室"); } else { System.out.println(name+":"+s); socketlist.sendall(name+":"+s);//廣播到客戶端 } } info=new StringBuffer(); info.append(msg.readLine()); s=info.toString(); }socketlist.sendall(name+":"+"bye");
//通知該連接對應的客戶端下線 out.println("bye"); socketlist.sendall(name+" 退出聊天室"); System.out.println(name+":"+"bye"); System.out.println(name+" 退出聊天室"); socketlist.dele(this.client); } catch (IOException e) { e.printStackTrace(); }finally { //關閉相關資源 try { if (out!=null) out.close(); if (msg!=null) msg.close(); if (client!=null) client.close(); }catch (Exception e) { e.printStackTrace(); } } } }
最后就是主函數了,它要做的就是監聽是否有用戶連接到服務器,有的話就為該用戶啟動一條新線程,並將該連接存儲到工具類的serverlist中
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; public class chatroom { public static void main(String[] args) throws UnsupportedEncodingException, IOException, InterruptedException { // TODO 自動生成的方法存根 ServerSocket server=new ServerSocket(9090); Socket client; while(true) { //等待用戶連接 client=server.accept(); //將連接存儲到工具類 socketlist.add(client); //開啟一個新線程 netthread thread=new netthread(client); thread.start(); InetAddress address=client.getInetAddress(); System.out.println("當前客戶端的IP:"+address.getHostAddress()); } } }