聊天室实现——使用Socket 和 ServerSocket实现聊天小项目

Posted Johnny*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊天室实现——使用Socket 和 ServerSocket实现聊天小项目相关的知识,希望对你有一定的参考价值。

ServerSocket API

构造方法

ServerSocket()	 //创建一个没有绑定端口的服务器套接字
ServerSocket(int port) 	//创建一个绑定端口(port)的套接字

常用API

SocketAddress 	getLocalSocketAddress() // 返回此socket绑定的端点的地址   
Socket accept()   //侦听并接收连接到本服务的客户端连接

多线程

建立固定线程个数的线程池

   //创建拥有10个线程的线程池:当客户端申请连接时,便为之分配一个线程
   ExecutorService executorService = Executors.newFixedThreadPool(10);

newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程。任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子
Java ExecutorService

Socket

构造方法

//
Socket()
Socket(String host,int port) //创建套接字并连接到指定Ip和端口号的远程服务器端
Socket(InetAddress address, int port)  //创建一个流套接字并将其连接到指定主机上的指定端口号。
InputStream  getInputStream() //返回该socket的输入流
OutputStream 	getOutputStream() //返回该socket的输出流

聊天小项目

服务端功能实现

  1. 维护功能:维护所有在线的客户端
  2. 注册功能:将客户端的名称添加到服务器的客户端集合(ONLINE_CLIENT_MAP,该Map维护一个<String userName,Socket client> 键值对)
  3. 私聊功能:客户端与指定客户端发送和接受数据
  4. 退出功能: 从服务器客户端集合移除客户端

客户端功能实现

  1. 注册功能: 创建Socket,给服务端发送注册执行
  2. 私聊功能: 客户端指定客户端发送数据,相应客户端接收数据
  3. 退出功能: 给服务器发送退出指令

实现思路

**第一步:**服务端要开启连接功能,并处于监听状态。客户端只需根据服务端的端口和IP地址即可发送连接的请求。
**第二步:**由于是多人聊天因此使用多线程奇数。开辟固定大小的线程池来控制客户端连接数量。

MulThreadServerHandler

package com.johnny.chatroom.web.controller;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 多线程服务器端
 */
public class MulThreadServerHandler {

    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        try {
            //1. 启动服务端
            serverSocket = new ServerSocket(2158);
            System.out.println("服务器启动……"+serverSocket.getLocalSocketAddress());

            //2. 创建线程池:当客户端申请连接时,便为之分配一个线程
            ExecutorService executorService = Executors.newFixedThreadPool(10);

            //3. 阻塞,侦听并接收连接到本服务的客户端连接
            while(true){
                Socket client =  serverSocket.accept();
                System.out.println("有客户端连接到服务器:"+client.getRemoteSocketAddress());
                executorService.execute(new ClientHandler(client));
                }
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }

    }
}

第三步: 服务端建立好连接之后,就需等待客户端连接。客户端也不单单是连接了服务器就行了,还有信息的交互,包括从服务端读取数据(ReadDataFromServer)和发送数据给服务端(WriteDataToServerThread)。由于读写两个过程可以同时进行,因此开启两个线程来实现。

发送数据给服务端

package com.johnny.chatroom.web.controller;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Scanner;

public class WriteDataToServerThread extends Thread{

    private final Socket client;
    public WriteDataToServerThread(Socket client){
        this.client = client;
    }

    @Override
    public void run() {
        OutputStream clientOutput = null;
        OutputStreamWriter writer = null;
        Scanner reader = null;
        try {
            clientOutput = client.getOutputStream();
            writer = new OutputStreamWriter(clientOutput);
            reader = new Scanner(System.in);
            while(true){
                System.out.println("输入信息(回车结束!):");
                String msg = reader.nextLine();
                writer.write(msg+"\\n");
                writer.flush();
                if(msg.equals("bye")){
                    // 表示客户端要关闭
                    client.close();
                    break; //没有这个,bye时,read会有异常,因为已知在while里等待输入
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(reader != null ) reader.close();
            try {
                if(clientOutput != null) clientOutput.close();
                if(writer != null ) writer.close();
            } catch (IOException e) { e.printStackTrace(); }

        }
    }
}

从服务端读取数据

package com.johnny.chatroom.web.controller;

import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.Scanner;

public class ReadDataFromServerThread extends Thread{
    private final Socket client;

    public ReadDataFromServerThread(Socket client) {
        this.client = client;
    }

    @Override
    public void run() {
        InputStream clientInput = null;
        try {
            clientInput = client.getInputStream();
            Scanner scanner = new Scanner(clientInput);
            while(true){
                String data = scanner.nextLine();
                System.out.println("来自服务器的消息:"+data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

第四步: 实现具体服务器客户端业务逻辑

package com.johnny.chatroom.web.controller;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class ClientHandler implements Runnable{
    //在线用户Map final 且为concurrentHashMap(线程安全)
    private static final Map<String, Socket> ONLINE_CLIENT_MAP = new ConcurrentHashMap<>();

    //单例模式
    private final Socket client;

    public ClientHandler(Socket client){
        this.client = client;
    }

    @Override
    public void run() {
        try {
            InputStream clientInput = client.getInputStream();
            Scanner scanner = new Scanner(clientInput);
            while (true){
                String data = scanner.nextLine();
                //注册
                if( data.startsWith("register:")){
                    String userName = data.split(":")[1];
                    register(userName);
                    continue;
                }

                //私聊
                if(data.startsWith("privateChat:")){
                    String[] segments = data.split(":");
                    String targetUserName = segments[1];
                    String message = segments[2];
                    privateChat(targetUserName,message);
                    continue;
                }
                
                //退出
                if(data.equals("bye")){
                    bye();
                    continue;
                }
                

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 客户端私聊
     * 往指定targetUserName发送消息
     * @param targetUserName
     * @param message
     */
    private void privateChat(String targetUserName, String message) {

        //获取目标客户socket
        Socket target = ONLINE_CLIENT_MAP.get(targetUserName);

        if( target == null)
            this.sendMessage(this.client,"用户不存在", false);
        else{
            String currentUserName = this.getCurrentUserName();
            this.sendMessage(target,message,true);
        }


    }

    /**
     * 注册过程就是往ONLINE_CLIENT_MAP添加<String userName, Socket><
     * @param userName
     */
    private void register(String userName) {

        ONLINE_CLIENT_MAP.put(userName, this.client);

        this.sendMessage(this.client, "恭喜"+userName+"注册成功", false);
    }

    /**
     * 往指定客户端(target)发送消息
     * @param target
     * @param message
     * @param prefix
     */
    private void sendMessage(Socket target, String message, boolean prefix) {
        String currentUserName = this.getCurrentUserName();
        OutputStream clientOutput = null;

        try {
            clientOutput = target.getOutputStream();
            OutputStreamWriter writer = new OutputStreamWriter(clientOutput);
            if(prefix){
                writer.write(""+currentUserName+":"+message+"\\n");

            }else{
                writer.write(message+"\\n");
            }
            //刷出缓存区 才能在控制台显示
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }//通讯时双方的IO都要一直打开着


    }

    /**
     * 返回用户名字
     * @return
     */
    private String getCurrentUserName() {
        Set<Map.Entry<String, Socket>> clientSet = ONLINE_CLIENT_MAP.entrySet();

        for( Map.Entry<String, Socket> entry: clientSet){
            if(entry.getValue().equals(this.client) ){
                return entry.getKey();
            }
        }
        return null;
    }

    /**
     * 退出
     */
    public void bye(){
        Set<Map.Entry<String, Socket>> clientSet = ONLINE_CLIENT_MAP.entrySet();
        for( Map.Entry<String, Socket> entry: clientSet){
            if(entry.getValue().equals(this.client) ){
                ONLINE_CLIENT_MAP.remove(entry.getKey());
                break;
            }
        }
    }

}

测试
开启两个进程测试

package com.johnny.chatroom.web.test;

import com.johnny.chatroom.web.controller.ReadDataFromServerThread;
import com.johnny.chatroom.web.controller.WriteDataToServerThread;

import java.io.IOException;
import java.net.Socket;

public class ClientThread1 {
    public static void main(String[] args) {

        try {
            Socket client = new Socket("0.0.0.0", 2158);

            //开启读写进程
            new WriteDataToServerThread(client).start();
            new ReadDataFromServerThread(client).start();

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

package com.johnny.chatroom.web.test;

import com.johnny.chatroom.web.controller.ClientHandler;
import com.johnny.chatroom.web.controller.ReadDataFromServerThread;
import com.johnny.chatroom.web.controller.WriteDataToServerThread;

import java.io.IOException;
import java.net.Socket;

public class ClientThread2 {
    public static void main(String[] args) {

        try {
            Socket client = new Socket("0.0.0.0", 2158);
            ClientHandler clientHandler = new ClientHandler(client);

            //开启读写进程
            new WriteDataToServerThread(client).start();
            new ReadDataFromServerThread(client).start();

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

大致流程:
执行MulThreadServerHandler:1、启动服务端,等待客户端的连接申请。serverSocket.accept()会阻塞,侦听并接收连接到本服务的客户端连接。
执行ClientThread1: 客户端向服务发起请求连接。
服务端接收到客户端的连接请求之后, serverSocket.accept()拿到客户端的Socket对象。
之后执行executorService.execute(new ClientHandler(client)),为该socket对象分配一个线程,该线程有ClientHandler对象。

error

异常:java.util.NoSuchElementException: No line found

可能是错误地关闭了Scanner reader

参考文章:
[java]JavaSE基础小项目:校园多人畅聊系统

以上是关于聊天室实现——使用Socket 和 ServerSocket实现聊天小项目的主要内容,如果未能解决你的问题,请参考以下文章

vue + socket.io实现一个简易聊天室

使用node.js实现多人聊天室(socket.ioB/S)

iOS使用socket实现聊天功能

Socket 多人聊天室的实现 (含前后端源码讲解)

使用socket实现简单聊天室

Socket实现多人聊天室-未完成