1.如何创建客户端和服务端,客户端如何连接服务端

创建服务端

ServerSocket serverSocket = new ServerSocket(8080);

1.1 创建客户端

/*构造器参数1:服务端IP地址
:服务端端口号
         */
        Socket socket = new Socket("127.0.0.1",8080);
        System.out.println("客户端创建完成....");

1.2 服务端获取客户端连接

System.out.println("等待客户端连接....");
        Socket accept = serverSocket.accept();//会让运行的线程进入阻塞状态
        System.out.println("出现客户端连接...");

异常:

1.1 端口被占用异常:
java.net.BindException: Address already in use: JVM_Bind

2 服务端获取客户端连接的方式

1.1 单次获取客户端连接

Socket accept = serverSocket.accept();

1.2 使用死循环嵌套,让服务器一直监听获取客户端连接

while(true){
    Socket accept = serverSocket.accept();//会让运行的线程进入阻塞状态
}

1.3 在获取连接后保存到List集合中,方便以后发送消息使用

/**

   * 以后会出现多个线程分别写入和读取操作(线程不安全问题)
   */

   List<Socket> clientList = Collections.synchronizedList(new ArrayList<>());

1.4 服务端有多个线程要做不同的事情,监听客户端连接放到新线程

//启动线程1,监听客户端的连接
        new Thread(){

            @Override
            public void run() {
                //获取客户端连接
                while(true){
                    System.out.println("等待客户端连接....");
                    Socket accept = null;//会让运行的线程进入阻塞状态
                    try {
                        accept = serverSocket.accept();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    clientList.add(accept);
                    System.out.println("出现客户端连接...已有【"+clientList.size()+"】个连接...");
                }
            }
        }.start();

3.服务端监听客户端断开

原理:服务端循环调用保存的客户端Socket对象去发送消息如果发送消息失败说明就断开连接

//2.线程2:监听客户端断开连接的线程
        new Thread(){
            @Override
            public void run() {
                System.out.println("服务端状态心跳监听开启...");
                while(true){
                    for(int i=0;i<clientList.size();i++){
                        Socket socket = clientList.get(i);
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        OutputStream os = null;
                        try {
                            socket.sendUrgentData(0xFF);//使用紧急数据发送方法心跳测试客户端你是否存在(让线程沉睡500毫秒)
//                            os = socket.getOutputStream();
//                            os.write("@##@".getBytes());//发送心跳数据
                        } catch (IOException e) {//发送失败客户端断开连接
                            //e.printStackTrace();
                            clientList.remove(socket);
                            System.out.println("客户端断开连接...还剩【"+clientList.size()+"】个连接");
                        }
                    }
                }
            }
        }.start();

4.服务端监听客户端发送的消息

//3.线程3:监听客户发送给服务端的消息
        new Thread(){
            @Override
            public void run() {
                System.out.println("服务端消息监听开启...");
                while(true){
                    for(int i=0;i<clientList.size();i++){
                        Socket socket = clientList.get(i);
                        try {
                            InputStream is = socket.getInputStream();
                            int available = is.available();
                            if(available>0){//正常接收到客户端消息
                                byte [] arr = new byte[available];

                                is.read(arr);

                                String str = new String(arr);

                                System.out.println("客户端说:"+str);
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }.start();

5.客户端与服务端发送消息,内容分类

(1)告诉服务器我上线了(告诉服务器的QQ是多少)

信息模板: GOON:QQ号码

目的:

将Socket对象存放到Map<String,Socket>集合中,key:QQ号,value:客户端Socket对象

/**
         * 存放key:QQ号码
         * value:Socket对象
         * 的map集合
         */
        Map<String, Socket> socketMap = Collections.synchronizedMap(new HashMap<>());

同时在断开连接的时候要添加一个判断去移除map集合元素

//e.printStackTrace();
                            clientList.remove(socket);
                            String qqNumber = getKeyByValue(socketMap, socket);
                            if(qqNumber!=null){
                                socketMap.remove(qqNumber);
                            }
                            System.out.println("客户端断开连接...还剩【"+clientList.size()+"】个连接");

socketMap根据值获取QQ号码的方法

public static synchronized String getKeyByValue(Map<String,Socket> map,Socket socket){

        Set<String> keys = map.keySet();

        for(String key:keys){
            Socket socket1 = map.get(key);
            if(socket1.equals(socket)){
                return key;
            }
        }

        return null;

    }

(2)告诉服务器我要发消息给谁?

信息模板:SEND:对方的QQ号码:要发送的信息

在之前的基础上改写“服务器接收客户端消息代码”

//3.线程3:监听客户发送给服务端的消息
        new Thread(){
            @Override
            public void run() {
                System.out.println("服务端消息监听开启...");
                while(true){
                    for(int i=0;i<clientList.size();i++){
                        Socket socket = clientList.get(i);
                        try {
                            InputStream is = socket.getInputStream();
                            int available = is.available();
                            if(available>0){//正常接收到客户端消息
                                byte [] arr = new byte[available];

                                is.read(arr);

                                String str = new String(arr);

                                String type = str.substring(0, 4);

                                //信息模板: GOON:QQ号码
                                if("GOON".equals(type)){//上线消息

                                    int i1 = str.indexOf(":");
                                    String qqStr = str.substring(i1, str.length());

                                    socketMap.put(qqStr,socket);

                                }else if("SEND".equals(type)){//发送消息
                                    //SEND:对方的QQ号码:要发送的信息
                                    //将SEND:去掉
                                    String newStr = str.substring(5, str.length());
                                    String msg = newStr.substring(newStr.indexOf(":")+1, str.length());
                                    String sendQQ = newStr.substring(0, newStr.indexOf(":"));
                                    System.out.println("转发信息:TO>"+sendQQ+"\n消息:"+sendQQ);
                                    //找出对应QQ号码的Socket对象,进行消息的转发
                                    Socket sendSocket = socketMap.get(sendQQ);

                                    OutputStream sendOs = sendSocket.getOutputStream();

                                    sendOs.write(msg.getBytes());

                                    sendOs.flush();
                                    System.out.println("转发成功!");

                                }

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

6.客户端代码编写

1.客户端监听服务转发的消息

new Thread(){
            @Override
            public void run() {
                System.out.println("客户端消息监听开启...");
                while(true){
                    try {
                        InputStream is = socket.getInputStream();
                        int available = is.available();
                        if(available>0){

                            byte [] arr = new byte[available];

                            is.read(arr);

                            String str = new String(arr);

                            System.out.println("---服务端转发:--------------------\n");
                            System.out.println("---"+str+"\n----------------------------");
                            
                        }


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

2.客户端要编写一个发送信息的方法

封装发送消息的方法:

/**
     * 发送消息给服务端
     * @param socket  发送消息的客户端对象
     * @param msg  信息内容
     */
    public static synchronized void sendMsg(Socket socket,String msg){

        try{
            OutputStream os = socket.getOutputStream();

            os.write(msg.getBytes());
            
            os.flush();
            
        }catch (IOException e){
            e.printStackTrace();
        }
        

    }

建立一个线程去可以让客户端一直发送消息

new Thread(){
            Scanner sc = new Scanner(System.in);
            @Override
            public void run() {
                System.out.println("客户端发送消息功能加载完成....");
                while(true){
                    System.out.println("请输入发送的QQ号码:");
                    String qq = sc.next();

                    System.out.println("请输入发送的消息:");
                    String next = sc.next();

                    String msg = "SEND:"+qq+":"+next;

                    sendMsg(socket,msg);

                }
            }
        }.start();

客户端要定义一个QQ号码:并且告诉服务器上线

//发送一个消息给服务端告诉我以上线,附上QQ号
        String QQNumber = UUID.randomUUID().toString().replaceAll("-","").substring(0,6).toUpperCase();
        //封装信息体  告诉上线:  GOON:QQ
        String goonMsg = "GOON:"+QQNumber;
        sendMsg(socket,goonMsg);
        System.out.println("我的QQ号码:"+QQNumber);