目录
1.创建一个模块,代表我们的项目:bb-chat-system
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,再群聊内容发送给服务端就完事了。