[Java]Socket API編寫一個簡單的私聊和羣聊

介紹

Socket,ServerSocket

Socket就是我們所說的套接字,主要由IP地址和端口來表示,IP即目標服務器的IP地址。ServerSocket主要用在服務端,作用是監聽服務器的某一個端口。通過ServerSocket的accept()可以得到一個客戶端Socket,再通過輸入輸出流可以對其進行讀寫,從而實現服務器和客戶端之間的交互。

消息格式

既然這裏是需要實現私聊和羣聊,自然需要規定數據格式,這裏我採用JSON的格式來進行數據傳輸,這樣的目的是可以用JSON解析工具(如FastJSON)方便地對消息進行解析,降低代碼編寫難度提高效率。

其他的問題

1,因爲需要實現收發信息,而且收和發兩個動作是無序的,所以需要收和發兩個動作需要單獨的線程來進行單獨處理。
2,實現私聊,服務器需要根據用戶的唯一標識ID來轉發信息,所以需要對Socket再次封裝。

編寫

用戶對象

主要用來表示用戶的基本信息。

/**
 * @author yintianhao
 * @createTime 2020/6/30 0:16
 * @description 用戶類
 */
public class User {

    private String nickname;
    private Integer id;

    public User(String nickname, Integer id) {
        this.nickname = nickname;
        this.id = id;
    }

    public User(Integer id){
        this.id = id;
    }

    //getter setter 略
}

消息對象

消息對象包括用戶發出的正文內容,消息種類(羣聊,私聊,初始化消息),消息來源(User對象),消息目的地(User對象)。通過對消息對象和JSON字符串相互轉化來實現消息的解析和生成。

/**
 * @author yintianhao
 * @createTime 2020/6/30 0:15
 * @description 消息對象
 */
public class Msg {

    //信息內容
    private String content;

    //信息種類,1羣聊,2私聊,3初始化消息
    private Integer type;

    private User from;

    private User to;

    public Msg(String content, Integer type, User from, User to){
        this.content = content;
        this.type = type;
        this.from = from;
        this.to = to;
    }
    //getter setter 略
}

服務端編寫

管道類

之前說過需要在Socket的基礎上進行再次封裝,封裝成管道類,管道用一個userId來唯一標識,也就是說一個管道可以看成一個用戶,在服務器啓動後,客戶端連接到服務器之後會發送一條初始化信息到服務端,從而在管道內的構造函數以內對初始化信息進行解析,整個管道的初始化到此完成。管道類另外一個作用就是實現讀寫分離。

/**
 * @author yintianhao
 * @createTime 2020/6/28 23:42
 * @description 管道類,實現讀寫分離。
 */
public class Channel implements Runnable{

    //log
    private static Logger logger = Logger.getLogger(Channel.class);

    //輸入輸出流
    private DataOutputStream dataOutputStream;
    private DataInputStream dataInputStream;

    //客戶端套接字
    private Socket client;

    //運行標誌
    private boolean isRunning;

    //用戶列表
    private CopyOnWriteArrayList<Channel> all;

    private Integer userId;

    public Channel(Socket client){
        //初始化
        logger.info("A client has connected");
        this.client = client;
        this.isRunning = true;
        try {
            this.dataInputStream = new DataInputStream(client.getInputStream());
            this.dataOutputStream = new DataOutputStream(client.getOutputStream());
        }catch (IOException e){
            logger.info(e.getMessage());
            //異常釋放資源
            release();
        }

        //初始化管道id
        String initMsg = receive();
        logger.info("init message -- "+initMsg);
        Msg msg = JSONUtil.getJsonObject(initMsg);
        if (msg.getType() == 3) {
            this.userId = Integer.parseInt(msg.getContent());
        }
    }

    public void setChannelList(CopyOnWriteArrayList<Channel> all){
        this.all = all;
    }

    //接收消息
    private String receive(){
        String msg = "";
        try {
            msg = dataInputStream.readUTF();
            logger.info("received msg -- "+msg);
        }catch (IOException e){
           logger.info(e.getMessage());
            //異常釋放資源
            release();
        }
        return msg;
    }

    //發送單條信息
    private void send(String content){
        try {
            logger.info("server transfer -- "+content);
            dataOutputStream.writeUTF(content);
            dataOutputStream.flush();
        }catch (IOException e){
            logger.info(e.getMessage());
            //異常釋放資源
            release();
        }
    }

    //羣聊
    private void sendOthers(String msg){
        logger.info("Send msg to others");
        logger.info("Channel list size -- "+all.size());
        for (Channel c:all){
            if(c!=this){
                c.send(msg);
            }
        }
    }

    private void sendOne(String content){
        logger.info("Send to someone");
        //轉成json對象
        Msg msg = JSON.parseObject(content,Msg.class);

        User from = msg.getFrom();
        User to = msg.getTo();
        logger.info("Message from "+from.getId());
        logger.info("Message to "+to.getId());

        for (Channel c:all){
            try {
                if (c.userId.intValue()==to.getId().intValue()){
                    c.send(content);
                    break;
                }
            }catch (NullPointerException e){
                logger.error(e.getMessage());
            }
        }
    }


    //釋放資源
    public void release(){
        //標誌改變
        isRunning = false;
        //關閉socket 輸入 輸出流
        StreamUtil.close(dataInputStream,dataOutputStream,client);
        logger.info("A client has released");
    }

    @Override
    public void run() {
        while(isRunning){
            String content = receive();
            Msg msg = JSON.parseObject(content,Msg.class);
            //通過Msg的type來判斷是私聊還是羣聊
            if (msg.getType()==1){
                //羣聊
                msg.setTo(null);
                String publicMsg = JSONUtil.getJsonString(msg);
                sendOthers(publicMsg);
                logger.info("It is a public message");
            }else if (msg.getType()==2){
                //私聊
                sendOne(content);
                logger.info("It is a private message");
            }else{
                logger.info("It is an initial message");
            }

        }
    }
}

啓動服務器

/**
 * @author yintianhao
 * @createTime 2020/6/29 1:16
 * @description
 */
public class Server {
    //logger
    private static Logger logger = Logger.getLogger(Server.class);

    public static void main(String[] args){
        logger.info("Server has started");
        try {
            ServerSocket server = new ServerSocket(8888);
            //創建容器
            CopyOnWriteArrayList<Channel> channelList = new CopyOnWriteArrayList<>();

            while(true){
                //客戶端套接字
                Socket client = server.accept();
                //新建一個管道
                Channel channel = new Channel(client);
                //加入管道列表
                channelList.add(channel);
                //管道設置自己的管道列表
                channel.setChannelList(channelList);
                //開啓線程服務一個管道
                Thread thread = new Thread(channel);
                thread.start();
            }
        } catch (IOException e) {
            logger.error(e.getMessage());
        }
    }
}

客戶端編寫

客戶端的管道有兩種,一種是發送管道,一種是接收管道。作用跟服務器的管道類似。由於這個demo是基於控制檯的,這裏用戶的ID和需要發送的消息類型都是根據控制檯輸入的。

發送管道

/**
 * @author yintianhao
 * @createTime 2020/6/29 1:31
 * @description 發送
 */
public class SendChannel implements Runnable{

    //控制檯輸入
    private BufferedReader console;

    //數據輸出流
    private DataOutputStream dos;

    //客戶端套接字
    private Socket client;

    //自身身份信息
    private User user;

    private Integer to;

    private boolean isRunning;

    private static Logger logger = Logger.getLogger(SendChannel.class);

    public SendChannel(Socket client,User user,Integer to){
        //初始化
        console = new BufferedReader(new InputStreamReader(System.in));
        this.client = client;
        this.isRunning = true;
        this.user = user;
        this.to = to;
        try {
            dos = new DataOutputStream(client.getOutputStream());
        } catch (IOException e) {
            release();
            logger.error(e.getMessage());
        }
        //發送id初始化管道
        Msg initMsg = new Msg(String.valueOf(user.getId()),3,user,null);
        send(JSONUtil.getJsonString(initMsg));
        logger.info("SendChannel has inited");
    }

    public void release(){
        isRunning = false;
        StreamUtil.close(console,dos,client);
    }

    private String getMsgFromConsole(){
        try {
            String content = console.readLine();
            //通過消息內容分割,strs[0]是消息類型
            String[] strs = content.split("-");
            Msg msg = new Msg(content,Integer.parseInt(strs[0]),user,new User(to));
            return JSON.toJSONString(msg);
        }catch (IOException e){
            logger.error(e.getMessage());
        }
        return "";
    }

    //發送消息
    private void send(String content){
        try {
            dos.writeUTF(content);
            dos.flush();
        }catch (IOException e){
            logger.error(e.getMessage());
            //異常釋放資源
            release();
        }
        logger.info("Send -- "+content);
    }

    public User getUser(){
        return user;
    }

    @Override
    public void run() {
        while(isRunning){
            String msg = getMsgFromConsole();
            if (!msg.equals("")){
                send(msg);
            }
        }
    }
}

接收管道

接收管道比較簡單,就是接收然後解析。

/**
 * @author yintianhao
 * @createTime 2020/6/29 1:31
 * @description 接收
 */
public class ReceiveChannel implements Runnable {

    private DataInputStream dos;

    private Socket client;

    private boolean isRunning;

    private static Logger logger = Logger.getLogger(ReceiveChannel.class);

    public ReceiveChannel(Socket client){


        this.client = client;
        isRunning = true;
        try {
            dos = new DataInputStream(client.getInputStream());
        } catch (IOException e) {
            logger.error(e.getMessage());
            release();
        }
        logger.info("ReceiveChannel has inited");
    }

    private void release(){
        isRunning = false;
        StreamUtil.close(dos,client);
    }

    private String getMsgFromChannel(){
        try {
            String msg = dos.readUTF();
            return msg;
        } catch (IOException e) {
            logger.error(e.getMessage());
            release();
            //e.printStackTrace();
        }
        return "";
    }
    @Override
    public void run() {
        while (isRunning){
            String content = getMsgFromChannel();
            Msg msg = JSON.parseObject(content,Msg.class);
            if (msg!=null){
                logger.info("Message from:"+msg.getFrom().getNickname()+"--"+msg.getContent());
            }
        }
    }
}

啓動客戶端

由於這裏我沒有選擇從CMD輸入用戶暱稱,所以我用了三個Client類來模擬客戶端,另外兩個交Client1,Client2,三個類的區別只是User對象的暱稱不同。

/**
 * @author yintianhao
 * @createTime 2020/6/28 23:38
 * @description 讀寫分離,封裝
 */
public class Client0 {

    private static Logger logger = Logger.getLogger(Client0.class);
    public static void main(String[] args){
        logger.info("Client0 start,Input your id and other id");
        try {
            Scanner scanner = new Scanner(System.in);
            String str = scanner.next();
            int from = Integer.parseInt(str.split("-")[0]);
            int to = Integer.parseInt(str.split("-")[1]);

            //連接
            Socket client = new Socket("127.0.0.1",8888);
            //發送信息的線程

            User user = new User("Yintianhao",from);

            SendChannel sendChannel = new SendChannel(client,user,to);

            Thread sendThread = new Thread(sendChannel);
            sendThread.start();

            ReceiveChannel receiveChannel = new ReceiveChannel(client);
            Thread receiveThread = new Thread(receiveChannel);
            receiveThread.start();

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

演示

錄了一個小視頻

總結

其實這個demo雖然私聊羣聊是實現了,但是其實是存在許多不足的,比如消息的確認到達機制,消息丟失怎麼辦,消息的持久化,這些我都沒有考慮進去,這陣子我也還在學習這方面的內容,希望以這個demo爲開始繼續完善吧。(博客同步:https://izzer.cn/archives/20200707)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章