首先制作一个客户端,界面如下:
使用方法:启动后,首先在登录编辑框输入一个昵称,然后点击登录,上面灰色区域是聊天窗,其中会显示你的登录提示,显示其他人发的消息。在的登录成功后,可以在下面的发送编辑框内编辑你要发的信息,点击发送就可以推送给当前所有登录中的用户,下线的方法就是发送“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()); } } }