Java-网络编程

目录

1.网络编程概述

 2.网络通信三要素

2.1 IP地址

2.2 端口

 2.3 协议

3.UDP通信

3.1 UDP一发一收

3.2.UDP多发多收

4.TCP通信

4.1 TCP一发一收

4.2 TCP多发多收

4.3 同时接收多个客户端的消息

 4.4 B/S架构的原理

6.实战项目-完成局域网内沟通软件的开发 

6.1 需求

6.2 技术栈

6.3 思路分析

1.创建一个模块,代表我们的项目:bb-chat-system

2.拿到系统需要的界面:Swing的代码

3. 定义一个App启动类:创建进入界面对象并展示。

4.分析系统的整体架构

开发服务端

客户端界面已经准备好了

5.先开发完整的服务端

 6.完善整个客户端程序的代码


1.网络编程概述

什么是网络编程?

  • 可以让设备中的程序与网络上其他设备中的程序进行数据交互(实现网络通信的)。

Java提供了哪些网络编程的解决方案?

java.net.*包下提供了网络编程的解决方案

 CS架构在商业领域中不太流行

 2.网络通信三要素

 

2.1 IP地址

package com.itheima.d1_inetAddress;

import java.net.InetAddress;

public class InetAddressDemo1 {
    public static void main(String[] args) throws Exception {
        // 目标:InetAddress类代表IP地址对象,用来操作IP地址。
        // 1、获取本机IP地址对象
        InetAddress ip = InetAddress.getLocalHost();
        System.out.println(ip.getHostAddress());
        System.out.println(ip.getHostName());

        // 2、指定获取对方主机的IP地址对象
        InetAddress ip2 = InetAddress.getByName("www.baidu.com");
        System.out.println(ip2.getHostAddress());
        System.out.println(ip2.getHostName());

        // 3、判断本机与该主机是否能够联通:ping
        System.out.println(ip2.isReachable(5000));
    }
}

2.2 端口

 2.3 协议

通信协议:网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。

为了让全球所有的设备都能够互联,就出现了开放式网络互联标准:OSI网络参考模型

 为什么要三次握手建立连接?

保证收发两端都是双工模式,传输数据会进行确认,以保证数据传输的可靠性。

3.UDP通信

3.1 UDP一发一收

 

package com.itheima.d2_udp1;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * 需求:一发 一收。
 */
public class Client {
    public static void main(String[] args) throws Exception {
        // 目标:客户端实现(发送端)
        // 1、创建发送端对象(抛韭菜的人)
        DatagramSocket socket = new DatagramSocket(); // 默认会分配端口

        // 2、创建一个数据包对象,负责封装要发送的数据。(盘子)
        /**
         * 参数一:要发送的数据,字节数组
         * 参数二:发送的数据大小
         * 参数三:目的地IP地址
         * 参数四:接收端端口号
         */
        byte[] buffer = "今晚一起啤酒、龙虾、小烧烤,约吗??".getBytes();
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getLocalHost(),
               8888); // 盘子

        // 3、把这一包数据发出去(抛出去韭菜)
        socket.send(packet);

        // 4、释放资源
        socket.close();

        System.out.println("客户端已经发送完毕!");
    }
}
package com.itheima.d2_udp1;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("===服务器启动成功===");
        // 目标:开始开发一个服务端程序。(接收端)
        // 1、创建接收端对象,注册端口(接收端人)
        DatagramSocket socket = new DatagramSocket(8888);

        // 2、创建一个数据包对象(韭菜盘子)
        byte[] buffer = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        // 3、接收数据(接收韭菜)
        socket.receive(packet); // 暂停等待!!

        // 4、把数据输出
        // 获取本轮真正接收到的字节数量
        int len = packet.getLength();

        //把buffer数组里从索引0开始,长度为len的字节转换为字符串
        String msg = new String(buffer,0,len);
        System.out.println(msg);

        // 获取发送端的IP和端口!!
        InetAddress ip = packet.getAddress();
        System.out.println("对方ip:" +ip.getHostAddress());
        System.out.println("对方端口:" + packet.getPort());

        // 5、释放资源
        socket.close();
    }
}

3.2.UDP多发多收

package com.itheima.d3_udp2;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

/**
 * 需求:多发多收
 */
public class Client {
    public static void main(String[] args) throws Exception {
        // 目标:客户端实现(发送端)
        // 1、创建发送端对象(抛韭菜的人)
        DatagramSocket socket = new DatagramSocket(); // 默认会分配端口,//随机端口,也可以自己指定

        Scanner sc = new Scanner(System.in);
        while (true) {
            // 2、创建一个数据包对象,负责封装要发送的数据。(盘子)
            System.out.println("请说:");
            String msg = sc.next();//你好 在干嘛
            //nextLine()可以获取整行输入包括空格,而next()只能获取一个单词

            if("exit".equals(msg)){
                System.out.println("退出成功!");
                socket.close();
                break;
            }

            byte[] buffer = msg.getBytes();
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getLocalHost(),
                   8888); // 盘子

            // 3、把这一包数据发出去(抛出去韭菜)
            socket.send(packet);
        }
    }
}
package com.itheima.d3_udp2;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("===服务器启动成功===");
        // 目标:开始开发一个服务端程序。(接收端)
        // 1、创建接收端对象,注册端口(接收端人)
        DatagramSocket socket = new DatagramSocket(8888);

        // 2、创建一个数据包对象(韭菜盘子)
        byte[] buffer = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        while (true) {
            // 3、接收数据(接收韭菜)
            socket.receive(packet); // 暂停等待!!

            // 4、把数据输出
            // 获取本轮收到多少字节。
            int len = packet.getLength();
            String msg = new String(buffer,0,len);
            System.out.println("收到:" + msg);

            // 获取发送端的IP和端口!!
            InetAddress ip = packet.getAddress();
            System.out.println("对方ip:" +ip.getHostAddress());
            System.out.println("对方端口:" + packet.getPort());
            System.out.println("-------------------------------------------------------------");
        }
    }
}

4.TCP通信

4.1 TCP一发一收

 

package com.itheima.d4_tcp1;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;

public class Client1 {
    public static void main(String[] args) throws Exception {
        // 目标:TCP通信客户端开发。 一发一收
        // 1、创建socket管道与服务端建立链接
        Socket socket = new Socket("127.0.0.1", 9999);

        // 2、从socket管道中得到一个字节输出流,往外写数据出去。
        OutputStream os = socket.getOutputStream();

        // 3、把低级流包装成特殊数据输出流
        DataOutputStream dos = new DataOutputStream(os);
        //为什么要包装成特殊数据输出流?
        //因为TCP通信,发送数据出去,要求必须是字节数组,而我们发送的数据,可能不是字节数组。
        //能够分行看清楚发送的数据类型,所以,需要特殊数据流进行数据的发送

        // 4、发数据出去给服务端
        dos.writeInt(12);
        dos.writeUTF("服务端您好,我是骚气的客户端,请问约吗?6666~~~~");
        dos.flush();

        // 5、释放资源
        socket.close();
    }
}

package com.itheima.d4_tcp1;

import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server1 {
    public static void main(String[] args) throws Exception {
        // 目标:实现TCP通信下一发一收:服务端开发
        System.out.println("==服务端启动成功===");
        // 1、创建服务端ServerSocket对象,绑定端口号,监听客户端连接
        ServerSocket serverSocket = new ServerSocket(9999);

        // 2、调用accept方法,阻塞等待客户端的socket链接请求
        // 一旦有客户端连接会返回一个Socket对象
        Socket socket = serverSocket.accept();

        // 3、从通信管道socket中得到一个字节输入流,读取客户端发送的数据
        InputStream is = socket.getInputStream();

        // 4、把字节输入流包装成特殊数据输入流
        DataInputStream dis = new DataInputStream(is);

        // 5、收数据
        System.out.println("收到1:" + dis.readInt());
        System.out.println("收到2:" + dis.readUTF());

        // 6.客户端的ip和端口(谁发给我的)
        System.out.println("客户端的ip:" + socket.getInetAddress().getHostAddress());
        System.out.println("客户端的端口:" + socket.getPort());
        // 6、释放资源
        socket.close();
    }
}

4.2 TCP多发多收

package com.itheima.d5_tcp2;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class Client1 {
    public static void main(String[] args) throws Exception {
        // 目标:TCP通信客户端开发。 多发多收
        // 1、创建socket管道与服务端建立链接
        Socket socket = new Socket("127.0.0.1", 9999);

        // 2、从socket管道中得到一个字节输出流,往外写数据出去。
        OutputStream os = socket.getOutputStream();

        // 3、把低级流包装成特殊数据输出流
        DataOutputStream dos = new DataOutputStream(os);

        Scanner sc = new Scanner(System.in);
        while (true) {
            // 4、发数据出去给服务端
            System.out.println("请说:");
            String msg = sc.nextLine();

            if("exit".equals(msg)){
                System.out.println("退出成功!");
                socket.close();
                break;
            }

            dos.writeUTF(msg);//发送数据
            dos.flush();
        }
    }
}
package com.itheima.d5_tcp2;

import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server1 {
    public static void main(String[] args) throws Exception {
        try {
            System.out.println("==服务端启动===");
            // 目标:掌握TCP通信下多发多收:服务端的开发。
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(9999);

            // 2、阻塞等待客户端的socket链接请求
            Socket socket = serverSocket.accept();
            System.out.println("上线了~~~");


            // 3、从通信管道socket中得到一个字节输入流
            InputStream is = socket.getInputStream();

            // 4、把字节输入流包装成特殊数据输入流
            DataInputStream dis = new DataInputStream(is);

            while (true) {
                // 5、收数据
                String msg = dis.readUTF();//等待读取客户端发送的数据
                System.out.println("收到:" + msg);
                //6.客户端的ip和端口(谁给我发的)
                System.out.println("客户端的ip端口:" + socket.getInetAddress().getHostAddress());
                System.out.println("客户端的端口:" + socket.getPort());
                System.out.println("------------------------------------------------------");
            }
        } catch (Exception e) {
            System.out.println("下线了~~~");
        }
    }
}

4.3 同时接收多个客户端的消息

目前我们开发的服务端程序,是否可以支持同时与多个客户端通信?

  • 不可以
  • 因为服务端现在只有一个主线程,只能处理一个客户端的消息。

 

 客户端:

package com.itheima.d6_tcp3;

import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class Client1 {
    public static void main(String[] args) throws Exception {
        // 目标:TCP通信客户端开发。 多发多收,
        System.out.println("客户端启动成功!");
        // 1、创建socket管道与服务端建立链接
        Socket socket = new Socket("127.0.0.1", 9999);

        // 2、从socket管道中得到一个字节输出流,往外写数据出去。
        OutputStream os = socket.getOutputStream();

        // 3、把低级流包装成特殊数据输出流
        DataOutputStream dos = new DataOutputStream(os);

        Scanner sc = new Scanner(System.in);
        while (true) {
            // 4、发数据出去给服务端
            System.out.println("请说:");
            String msg = sc.nextLine();

            if("exit".equals(msg)){
                System.out.println("退出成功!");
                socket.close();
                break;
            }

            dos.writeUTF(msg);
            dos.flush();
        }
    }
}

服务器端:

package com.itheima.d6_tcp3;

import java.net.ServerSocket;
import java.net.Socket;

public class Server1 {
    public static void main(String[] args) throws Exception {
        try {
            System.out.println("==服务端启动===");
            // 目标:掌握TCP通信服务端的开发。
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(9999);

            while (true) {
                // 2、调用accept方法,阻塞等待客户端的socket链接请求
                //一旦有客户端连接,accept方法就会返回一个Socket对象,此时服务端和客户端已经建立连接。
                Socket socket = serverSocket.accept();
                System.out.println(socket.getInetAddress().getHostAddress() + " ==> 上线了~~~");

                // 3、把这个客户端管道交给一个独立的子线程来处理。
                new ServerReaderThread(socket).start();
            }

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

 服务端读数据的线程:

package com.itheima.d6_tcp3;

import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;

/**
 * 服务端读数据的线程
 */
public class ServerReaderThread extends Thread{
    private Socket socket;

    public ServerReaderThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 3、从通信管道socket中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成特殊数据输入流
            DataInputStream dis = new DataInputStream(is);

            while (true) {
                // 5、收数据
                System.out.println("收到:" + dis.readUTF());
                System.out.println("谁发的:" + socket.getInetAddress().getHostAddress());
                System.out.println("它的端口:" + socket.getPort());
                System.out.println("------------------------------------------------------");
            }

        } catch (Exception e) {
            System.out.println(socket.getInetAddress().getHostAddress() + " ==> 下线了~~~");
        }
    }
}

本次是如何实现服务端同时接收多个客户端的消息的?

  • 主线程定义了循环负责接收客户端Socket管道连接
  • 每接收到一个Socket通信管道后分配一个独立的线程负责处理它

 4.4 B/S架构的原理

package com.itheima.d8_bs;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

public class Server1 {
    public static void main(String[] args) throws Exception {
        try {
            System.out.println("==服务端启动===");
            // 目标:掌握TCP通信服务端的开发。
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(8080);
            // 创建线程池
            ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.MINUTES,
                    new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());


            while (true) {
                // 2、阻塞等待客户端的socket链接请求
                Socket socket = serverSocket.accept();
                System.out.println(socket.getInetAddress().getHostAddress() + " ==> 上线了~~~");

                // 3、把这个客户端管道包装成一个任务交给线程池处理
                pool.execute(new ServerReaderRunnable(socket));
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.itheima.d8_bs;

import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;

/**
 * 服务端读数据的线程
 */
public class ServerReaderRunnable implements Runnable{
    private Socket socket;

    public ServerReaderRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //给当前对应的浏览器管道相应一个网页数据回去
            OutputStream os = socket.getOutputStream();
            //通过字节输出流包装写出去数据给浏览器
            //把字节输出流包装成打印流
            PrintStream ps = new PrintStream(os);
            //写响应的网页数据出去
            ps.println("HTTP/1.1 200 OK");
            ps.println("Content-Type:text/html;charset=UTF-8");
            ps.println(); // 必须换行
            ps.println("<div style='color:red;font-size:120px;'>牛逼的就业班147期~</div>");
            ps.flush();

            //释放资源
            ps.close();
            socket.close();
        } catch (Exception e) {
            System.out.println(socket.getInetAddress().getHostAddress() + " ==> 下线了~~~");
        }
    }
}

6.实战项目-完成局域网内沟通软件的开发 

6.1 需求

展示一个用户的登录界面,这个界面只要求用户输入自己聊天的昵称就可以了。

在登录进入后,站是一个群聊的窗口,这个窗口,展示在线人数,展示消息展示框,消息输入框,发送按钮。可以实现群聊。实现实时展示在线人数。完全做到即使通讯功能。

6.2 技术栈

1.GUI编程技术:Swing

2.网络编程

3.面向对象设计

4.常用PI

6.3 思路分析

1.创建一个模块,代表我们的项目:bb-chat-system

2.拿到系统需要的界面:Swing的代码

  • 登录界面:这个界面只要求用户输入自己聊天的昵称就可以了
package com.itheima.ui;

import javax.swing.*;
import java.awt.*;

public class ChatEntryFrame extends JFrame {

    private JTextField nicknameField;
    private JButton enterButton;
    private JButton cancelButton;

    public ChatEntryFrame() {
        setTitle("局域网聊天室");
        setSize(350, 150);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setResizable(false); // 禁止调整大小

        // 设置背景颜色
        getContentPane().setBackground(Color.decode("#F0F0F0"));

        // 创建主面板并设置布局
        JPanel mainPanel = new JPanel(new BorderLayout());
        mainPanel.setBackground(Color.decode("#F0F0F0"));
        add(mainPanel);

        // 创建顶部面板
        JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
        topPanel.setBackground(Color.decode("#F0F0F0"));

        // 标签和文本框
        JLabel nicknameLabel = new JLabel("昵称:");
        nicknameLabel.setFont(new Font("楷体", Font.BOLD, 16));
        nicknameField = new JTextField(10);
        nicknameField.setFont(new Font("楷体", Font.PLAIN, 16));
        nicknameField.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createMatteBorder(1, 1, 1, 1, Color.GRAY),
                BorderFactory.createEmptyBorder(5, 5, 5, 5)
        ));

        topPanel.add(nicknameLabel);
        topPanel.add(nicknameField);
        mainPanel.add(topPanel, BorderLayout.NORTH);

        // 按钮面板
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
        buttonPanel.setBackground(Color.decode("#F0F0F0"));

        enterButton = new JButton("进入");
        enterButton.setFont(new Font("楷体", Font.BOLD, 16));
        enterButton.setBackground(Color.decode("#007BFF"));
        enterButton.setForeground(Color.WHITE);
        enterButton.setBorderPainted(false);
        enterButton.setFocusPainted(false);

        cancelButton = new JButton("取消");
        cancelButton.setFont(new Font("楷体", Font.BOLD, 16));
        cancelButton.setBackground(Color.decode("#DC3545"));
        cancelButton.setForeground(Color.WHITE);
        cancelButton.setBorderPainted(false);
        cancelButton.setFocusPainted(false);

        buttonPanel.add(enterButton);
        buttonPanel.add(cancelButton);
        mainPanel.add(buttonPanel, BorderLayout.SOUTH);

        // 添加监听器
        enterButton.addActionListener(e -> {
            String nickname = nicknameField.getText();
            if (!nickname.isEmpty()) {
                // 进入聊天室逻辑,这里仅关闭窗口示例,实际需补充连接等逻辑
                dispose();
            } else {
                JOptionPane.showMessageDialog(this, "请输入昵称!");
            }
        });

        cancelButton.addActionListener(e -> System.exit(0));

        this.setVisible(true);
    }

    public static void main(String[] args) {
        new ChatEntryFrame();
    }
}
  • 获取系统需要的聊天界面。
package com.itheima.ui;

import javax.swing.*;
import java.awt.*;

public class ClientChatFrame extends JFrame {
    public JTextArea smsContent = new JTextArea(23, 50);
    private JTextArea smsSend = new JTextArea(4, 40);
    public JList<String> onlineUsers = new JList<>();
    private JButton sendBn = new JButton("发送");

    public ClientChatFrame() {
        initView();
        this.setVisible(true);
    }
    private void initView() {
        this.setSize(700, 600);
        this.setLayout(new BorderLayout());
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置关闭窗口时退出程序
        this.setLocationRelativeTo(null);// 设置窗口居中显示

        // 设置窗口背景色
        this.getContentPane().setBackground(new Color(0xf0, 0xf0, 0xf0));

        // 设置字体
        Font font = new Font("SimKai", Font.PLAIN, 14);

        // 消息内容框
        smsContent.setFont(font);
        smsContent.setBackground(new Color(0xdd, 0xdd, 0xdd));
        smsContent.setEditable(false);

        // 发送消息框
        smsSend.setFont(font);
        smsSend.setWrapStyleWord(true);
        smsSend.setLineWrap(true);

        // 在线用户列表
        onlineUsers.setFont(font);
        onlineUsers.setFixedCellWidth(120);
        onlineUsers.setVisibleRowCount(13);

        // 创建底部面板
        JPanel bottomPanel = new JPanel(new BorderLayout());
        bottomPanel.setBackground(new Color(0xf0, 0xf0, 0xf0));

        // 消息输入框(带滚动面板)
        JScrollPane smsSendScrollPane = new JScrollPane(smsSend);
        smsSendScrollPane.setBorder(BorderFactory.createEmptyBorder());
        smsSendScrollPane.setPreferredSize(new Dimension(500, 50));

        // 发送按钮样式设置
        sendBn.setFont(font);
        sendBn.setBackground(Color.decode("#009688"));
        sendBn.setForeground(Color.WHITE);

        // 按钮面板(放置发送按钮)
        JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 5));
        btns.setBackground(new Color(0xf0, 0xf0, 0xf0));
        btns.add(sendBn);

        // 将消息输入框、按钮面板添加到底部面板
        bottomPanel.add(smsSendScrollPane, BorderLayout.CENTER);
        bottomPanel.add(btns, BorderLayout.EAST);

        // 用户列表面板(带滚动面板)
        JScrollPane userListScrollPane = new JScrollPane(onlineUsers);
        userListScrollPane.setBorder(BorderFactory.createEmptyBorder());
        userListScrollPane.setPreferredSize(new Dimension(120, 500));

        // 中心消息面板(带滚动面板)
        JScrollPane smsContentScrollPane = new JScrollPane(smsContent);
        smsContentScrollPane.setBorder(BorderFactory.createEmptyBorder());

        // 将所有组件添加到主窗口
        this.add(smsContentScrollPane, BorderLayout.CENTER);
        this.add(bottomPanel, BorderLayout.SOUTH);
        this.add(userListScrollPane, BorderLayout.EAST);
    }

    // 主方法用于测试界面(实际使用可在其他地方调用 initView() 初始化)
    public static void main(String[] args) {
        new ClientChatFrame();
    }
}

3. 定义一个App启动类:创建进入界面对象并展示。

public class App {
    public static void main(String[] args) {
        new ChatEntryFrame();
    }
}

4.分析系统的整体架构

开发服务端
  • 接收客户端的管道链接。
  • 接收登录信息,接收昵称信息。
  • 服务端也可能是接收客户端发送过来的群聊消息。
  • 服务端存储全部在线的socket管道,以便到时候知道哪些客户端在线,以便为这些客户端转发消息。(经典端口转发思想)
  • 如果服务端收到了登录消息,接收昵称,然后更新所有客户端的在线人数列表
  • 如果服务端收到了群聊消息,需要接收这个人的消息,再转发给所有客户端展示这个消息。
客户端界面已经准备好了

5.先开发完整的服务端

  • 第一步:创建一个服务端的项目:bb-chat-server
  • 第二步:创建一个服务端启动类,启动服务器等待客户端的连接
public class Server {
    public static void main(String[] args) {
        System.out.println("启动服务端系统......");
        try {
            //1.注册端口
            ServerSocket serverSocket = new ServerSocket(Constant.PORT);
            //2.主线程负责接收客户端的连接请求
            while (true) {
                //3.调用accept方法,阻塞等待客户端的连接,获取到客户端的socket管道
                System.out.println("等待客户端连接...");
                //把接收到的管道交给一个变量记录客户端的管道
                Socket socket =serverSocket.accept();
                System.out.println("一个客户端连接了...");

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 第三步:把这个管道交给一个独立的线程来处理:以便支持很多客户端可以同时进来通信
public class Server {
    public static void main(String[] args) {
        System.out.println("启动服务端系统......");
        try {
            //1.注册端口
            ServerSocket serverSocket = new ServerSocket(Constant.PORT);
            //2.主线程负责接收客户端的连接请求
            while (true) {
                //3.调用accept方法,阻塞等待客户端的连接,获取到客户端的socket管道
                System.out.println("等待客户端连接...");
                
                //4.把接收到的管道交给一个变量记录客户端的管道
                Socket socket =serverSocket.accept();
                //5.创建一个线程,把socket传递给线程,启动线程
                new ServerReaderThread(socket).start();
                
                System.out.println("一个客户端连接了...");

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  •  第四步:定义一个集合容器存储所有登录进来的客户端管道,以便将来群发消息给他们

                 --这个集合只需要一个记住所有在线的客户端socket

//定义一个集合容器存储所有登录进来的客户端管道,以便将来群发消息给他们
//定义一个Map集合,键是存储客户端的管道,值是存储这个管道的用户名称(名称有可能重复,就设置为值)
public static Map<Socket,String> onlineSockets = new HashMap<>();
  • 第五步:服务端接收登陆消息/群聊消息

接收的消息可能有很多种类型:1.登录消息(包含昵称)2.群聊消息 3.私聊消息
所以客户端必须声明协议发送消息
比如客户端先发1.代表接下来是登录消息
比如客户端先发2.代表接下来是群聊消息
比如客户端先发3.代表接下来是私聊消息

先接收一个整数,再判断,再区别对待。

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            //接收的消息可能有很多种类型:1.登录消息(包含昵称)2.群聊消息 3.私聊消息
            //所以客户端必须声明协议发送消息
            //比如客户端先发1.代表接下来是登录消息
            //比如客户端先发2.代表接下来是群聊消息
            //比如客户端先发3.代表接下来是私聊消息
            //先从socket管道中接收客户端发送来的消息类型编号
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            int type = dis.readInt();
            switch (type){
                case 1:
                    //客户端发来了登录消息,接下来要接收昵称数据,再更新全部在线客户端的在线人数列表
                    break;
                case 2:
                    //客户端发来了群聊消息,接下来要接收群聊消息内容,再把群聊消息发送给全部在线客户端
                    break;
                case 3:
                    //客户端发来了私聊消息,接下来要接收私聊消息内容,再把私聊消息发送给指定的在线客户端
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 第六步:实现服务端的登录消息接收
package com.itheima;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.util.Collection;

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            //接收的消息可能有很多种类型:1.登录消息(包含昵称)2.群聊消息 3.私聊消息
            //所以客户端必须声明协议发送消息
            //比如客户端先发1.代表接下来是登录消息
            //比如客户端先发2.代表接下来是群聊消息
            //比如客户端先发3.代表接下来是私聊消息
            //先从socket管道中接收客户端发送来的消息类型编号
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            int type = dis.readInt();// 1、2、3
            switch (type){
                case 1:
                    //客户端发来了登录消息,接下来要接收昵称数据,再更新全部在线客户端的在线人数列表
                    String nickname = dis.readUTF();
                    //把这个登录成功的客户端socket存入到在线集合。
                    Server.onlineSockets.put(socket,nickname);
                    //更新全部客户端的在线人数列表。
                    updateClientOnlineUsersList();
                    break;
                case 2:
                    //客户端发来了群聊消息,接下来要接收群聊消息内容,再把群聊消息发送给全部在线客户端
                    break;
                case 3:
                    //客户端发来了私聊消息,接下来要接收私聊消息内容,再把私聊消息发送给指定的在线客户端
                    break;
            }
        } catch (Exception e) {
            System.out.println("客户端下线了:" + socket.getInetAddress().getHostAddress());
            Server.onlineSockets.remove(socket);//把下线的客户端socket从在线集合中移除
        }
    }

    private void updateClientOnlineUsersList() {
        //更新全部客户端的在线人数列表
        //拿到全部在线客户端的用户名称,把这些名称转发给全部在线socket管道。
        //1.拿到当前全部在线用户名称
        Collection<String> onLineUsers = Server.onlineSockets.values();
        //2.把这个集合中的所有用户都推送给全部客户端socket管道
        for (Socket socket : Server.onlineSockets.keySet()) {
            try {
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                dos.writeInt(1);//1代表告诉客户端接下来是在线人数列表信息 2.代表发的是群聊消息
                dos.writeInt(onLineUsers.size());//告诉客户端,接下来要发多少个用户名称
                for (String user : onLineUsers) {
                    dos.writeUTF(user);
                }
                dos.flush();//刷新管道,确保数据发送给全部在线客户端
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
  •  第七步:接收客户端的群聊消息

 线程每收到一个客户端群聊消息,就应该把这个消息转发给全部在线的客户端对应的socket管道

package com.itheima;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            //接收的消息可能有很多种类型:1.登录消息(包含昵称)2.群聊消息 3.私聊消息
            //所以客户端必须声明协议发送消息
            //比如客户端先发1.代表接下来是登录消息
            //比如客户端先发2.代表接下来是群聊消息
            //比如客户端先发3.代表接下来是私聊消息
            //先从socket管道中接收客户端发送来的消息类型编号
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            while (true) {
                int type = dis.readInt();// 1、2、3
                switch (type){
                    case 1:
                        //客户端发来了登录消息,接下来要接收昵称数据,再更新全部在线客户端的在线人数列表
                        String nickname = dis.readUTF();
                        //把这个登录成功的客户端socket存入到在线集合。
                        Server.onlineSockets.put(socket,nickname);
                        //更新全部客户端的在线人数列表。
                        updateClientOnlineUsersList();
                        break;
                    case 2:
                        //客户端发来了群聊消息,接下来要接收群聊消息内容,再把群聊消息发送给全部在线客户端
                        String msg = dis.readUTF();
                        sendMsgToAll(msg);
                        break;
                    case 3:
                        //客户端发来了私聊消息,接下来要接收私聊消息内容,再把私聊消息发送给指定的在线客户端
                        break;
                }
            }
        } catch (Exception e) {
            System.out.println("客户端下线了:" + socket.getInetAddress().getHostAddress());
            Server.onlineSockets.remove(socket);//把下线的客户端socket从在线集合中移除
            updateClientOnlineUsersList();//更新全部客户端的在线人数列表
        }
    }

    //给全部在线socket推送当前客户端发来的消息
    private void sendMsgToAll(String msg) {
        //一定要拼装好这个消息,再发给全部在线socket。
        StringBuilder sb = new StringBuilder();
        //调用get方法,依据键来获取对应的值
        String name = Server.onlineSockets.get(socket);

        //获取当时间
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EEE a");
        String nowStr = dtf.format(now);

        StringBuilder msgResult = sb.append(name).append(" ").append(nowStr).append("\r\n")
                .append(msg).append("\r\n");
        //推送给全部客户端socket
        for (Socket socket : Server.onlineSockets.keySet()) {
            try {
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                dos.writeInt(2);//1代表告诉客户端接下来是在线人数列表信息 2.代表发的是群聊消息
                dos.writeUTF(msgResult.toString());
                dos.flush();//刷新管道,确保数据发送给全部在线客户端
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void updateClientOnlineUsersList() {
        //更新全部客户端的在线人数列表
        //拿到全部在线客户端的用户名称,把这些名称转发给全部在线socket管道。
        //1.拿到当前全部在线用户名称
        Collection<String> onLineUsers = Server.onlineSockets.values();
        //2.把这个集合中的所有用户都推送给全部客户端socket管道,keySet拿到的是所有键的Set集合
        for (Socket socket : Server.onlineSockets.keySet()) {
            try {
                DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
                dos.writeInt(1);//1代表告诉客户端接下来是在线人数列表信息 2.代表发的是群聊消息
                dos.writeInt(onLineUsers.size());//告诉客户端,接下来要发多少个用户名称
                for (String user : onLineUsers) {
                    dos.writeUTF(user);
                }
                dos.flush();//刷新管道,确保数据发送给全部在线客户端
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

 6.完善整个客户端程序的代码

 第一步:从登录界面开始:完成了登录,完成了socket管道传给消息聊天界面

给这个进入按钮绑定一个点击事件监听器,让他可以点击,一旦点击了,获取到昵称,然后立即请求与服务器端的socket管道连接。并立即发送1,发送昵称。

再展示客户端的聊天界面:接收到了昵称,接收到了属于自己客户端的socket通信管道

 第二步:立即在消息聊天界面,立即读取客户端socket管道从服务端发来的在线人数更新消息/群聊消息

  • 交给一个独立的线程专门负责读取客户端socket从服务端收到的在线人数更新数据和群聊数据。
  • 收到消息先判断消息的类型,判断是在线人数更新消息还是群聊消息,分开处理。

第三步:接收群聊消息

  • 接收消息类型2,接收群聊数据,展示到界面的面板上去即可

第四步:发送群聊消息

  • 给发送按钮绑定一个点击事件,获取输入框的消息内容,先发送2,再群聊内容发送给服务端就完事了。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值