网络编程
可以让设备中的程序与网络上其他设备中运行的程序进行数据交互的技术(实现网络通信)
1.基本通信架构
- CS架构:client(客户端) / server(客户端)
- BS架构: Browser(浏览器) / Server(服务端)
2.网络编程三要素
1.IP
设备在网络中的唯一标识
- IPv4:使用32位地址,以点分十进制表示
- IPv6: 使用128位地址,分成8段,每段每四位编码成一个十六进制位表示,以冒分十六进制表示
IP域名 - 用于在互联网识别和定位网站的人类可读的名称
例如:www.baidu.com、www.itheima.com - DNA域名解析
- 当第一次连接域名时,设备的DNS是无法识别域名的,需要从公网上的DNS服务器解析过来,保存到本地设备的DNS,后面再连接就可以直接从本机设备解析连接
- 常用IP命令
- ipconfig:查看本机IP地址
- ping IP地址:检查网络是否连通
- Java提供的IP接口
package com.itheima.inetaddressdemo;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class demo1 {
public static void main(String[] args) {
try {
//获取本机IP对象
InetAddress ip = InetAddress.getLocalHost();
System.out.println(ip.getHostAddress());
System.out.println(ip.getHostName());
//获取对方IP对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostAddress());
System.out.println(ip2.getHostName());
//查看本机是否与目标IP连通
System.out.println(ip2.isReachable(5000));
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.端口
应用程序在设备中的唯一标识
- 用来标记正在计算机设备上运行的应用程序,被规定为一个16的二进制,范围为 0~65535
3.协议
是连接和数据在网络中传输的规则
- 网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议
- UDP(User DatagramProtocol):用户数据报协议
- TCP(Transmission Control Protocol) :传输控制协议
1) UDP协议
- 特点:无连接、不可靠通信、通信效率高
- 不事先建立连接,数据按照包发,一包数据包含:自己IP、端口、目的地IP、端口、数据(限制在16KB内)等
发送方不管对方是否在线,数据在中间丢失也不管,接收方收到数据也不返回确认,故不可靠 - 常见应用场景: 视频直播、语言通话
2)TCP协议
- 特点:面向连接、可靠通信
- 目的:在不可靠的信道上实现可靠的数据传输
- 实现步骤:三次握手建立可靠连接、传输数据进行确认、四次挥手断开连接
1.三次握手
- 目的:确保通信双方都具备收发消息的能力
2.传输数据确认
3.四次挥手
- 目的:确保通信双方收发消息都已经完成
3.UDP通信实现
1) 一发一收
UDP通信Server端实现:
package com.itheima.udpconnet2;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class udpClientDemo {
public static void main(String[] args) throws Exception {
//目标:实现UDP通信,服务端开发
System.out.println("===服务端启动了===");
//1.创建服务端对象:接韭菜的人,接到哪个盘子
DatagramSocket socket = new DatagramSocket(8080);
//2.创建空数据包对象:准备盘子
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
////3.接收数据包
socket.receive(packet);
//获取当前接收到的数据长度
int len = packet.getLength(); //韭菜盘子知道它装了多少韭菜
String ip = packet.getAddress().getHostAddress();
int port = packet.getPort();
String msg = new String(buf, 0, len); //接收多少打印多少
System.out.println("服务端收到了:" + msg);
System.out.println("对方IP:" + ip + " port:" + port);
//4.关闭管道
socket.close();
}
}
UDP通信Client端实现:
package com.itheima.udpconnet2;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class udpServerDemo {
public static void main(String[] args) throws Exception {
//目标:实现UDP通信,客户端开发
System.out.println("===客户端启动了===");
//1.创建客户端对象: 抛韭菜的人
DatagramSocket socket = new DatagramSocket();
//2.准备数据:准备韭菜
byte[] buf = "你好".getBytes();
//3.创建数据包对象:将数据打包(创建盘子)
/*
public DatagramPacket(byte[] buf, int length,
InetAddress address, int port) {
* 参数1:发送的数据,字节数组(韭菜)
* 参数2:发送的字节长度
* 参数3:目的地的IP地址
* 参数4:服务端程序的端口号
*/
DatagramPacket packet = new DatagramPacket(buf, buf.length, InetAddress.getLocalHost(),8080);
//4.发送数据包:抛韭菜的动作
socket.send(packet);
System.out.println("数据发送成功!");
//5.关闭通信管道
socket.close();
}
}
2)多发多收
Server端多收
package com.itheima.udpconnet2;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class udpClientDemo {
public static void main(String[] args) throws Exception {
//目标:实现UDP通信,服务端开发
System.out.println("===服务端启动了===");
//1.创建服务端对象:接韭菜的人,接到哪个盘子
DatagramSocket socket = new DatagramSocket(8080);
//2.创建空数据包对象:准备盘子
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
while (true) {
////3.接收数据包
socket.receive(packet); //阻塞式接收数据
//获取当前接收到的数据长度
int len = packet.getLength(); //韭菜盘子知道它装了多少韭菜
String ip = packet.getAddress().getHostAddress();
int port = packet.getPort();
String msg = new String(buf, 0, len); //接收多少打印多少
System.out.println("服务端收到了:" + msg);
System.out.println("对方IP:" + ip + " port:" + port);
}
}
}
Client端多发
package com.itheima.udpconnet2;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class udpServerDemo {
public static void main(String[] args) throws Exception {
//目标:实现UDP多发多收通信,客户端开发
System.out.println("===客户端启动了===");
DatagramSocket socket = new DatagramSocket();
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String msg = sc.nextLine();
if (msg.equals("exit")) {
System.out.println("退出聊天");
socket.close();
break;
}
byte[] buf = msg.getBytes();
DatagramPacket packet = new DatagramPacket(buf, buf.length, InetAddress.getLocalHost(),8080);
socket.send(packet);
System.out.println("数据发送成功!");
}
}
}
4.TCP通信实现
1)一发一收
TCP通信Server端实现
步骤:
- 1.创建ServerSocket通信管道对象
- 2.调用accept方法,阻塞式等待客户端连接,一旦创建连接,返回socket对象
- 3.从socket管道中获取输入流
- 4.将低级输入流包装成客户端对应的特殊输入流
- 5.从输入流中读取数据内容,并获取对方的IP和端口
- 6.关闭通信管道,释放资源(一般不关)
package com.itheima.tcpconnet;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class tcpServerDemo {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信,服务端开发
System.out.println("===服务端启动===");
//1.创建ServerSocket通信管道对象,
ServerSocket serverSocket = new ServerSocket(9999);
//2.调用accept方法,阻塞式等待客户端连接,一旦有客户端连接,返回一个socket对象
Socket socket = serverSocket.accept();
//3.连接成功之后,从通信管道获取输入流对象
InputStream is = socket.getInputStream();
//4.将低级输入流包装成特殊输入流
DataInputStream dis = new DataInputStream(is);
//5.读取数据
int id = dis.readInt();
String msg = dis.readUTF();
System.out.println("id:" + id + "\tmsg:" + msg);
String address = socket.getInetAddress().getHostAddress();
int port = socket.getPort();
System.out.println("客户端的ip=" + address + ", port=" + port);
//6.关闭通信管道,释放资源
socket.close();
}
}
TCP通信Client端实现
步骤:
- 1.创建Socket对象,传入接入的服务端IP和端口
- 2.连接成功之后,从socket通信管道获取输出流
- 3.将低级输出流包装成特殊流,方便传输不同的数据类型
- 4.输出数据
- 5.关闭通信管道,释放资源
package com.itheima.tcpconnet;
import java.io.*;
import java.net.Socket;
public class tcpClientDemo {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信, 客户端开发
System.out.println("===客户端启动===");
//1.创建通信管道
Socket socket = new Socket("127.0.0.1", 9999);
//2.获取通信管道的字节输出流
OutputStream os = socket.getOutputStream();
//3.将低级输出流包装成特殊流传输数据
DataOutputStream dos = new DataOutputStream(os);
dos.writeInt(1);
dos.writeUTF("你好");
//4.关闭通信管道,释放资源
socket.close();
}
}
2)多发多收
Server端
package com.itheima.tcpconnet;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class tcpServerDemo {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信,服务端开发
System.out.println("===服务端启动===");
//1.创建ServerSocket通信管道对象,
ServerSocket serverSocket = new ServerSocket(9999);
//2.调用accept方法,阻塞式等待客户端连接,一旦有客户端连接,返回一个socket对象
Socket socket = serverSocket.accept();
//3.连接成功之后,从通信管道获取输入流对象
InputStream is = socket.getInputStream();
//4.将低级输入流包装成特殊输入流
DataInputStream dis = new DataInputStream(is);
while (true) {
String msg = dis.readUTF();
System.out.println("msg:" + msg);
String address = socket.getInetAddress().getHostAddress();
int port = socket.getPort();
System.out.println("客户端的ip=" + address + ", port=" + port);
System.out.println("=======================================");
}
}
}
Client端
package com.itheima.tcpconnet;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class tcpClientDemo {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信, 客户端开发
System.out.println("===客户端启动===");
//1.创建通信管道
Socket socket = new Socket("127.0.0.1", 9999);
//2.获取通信管道的字节输出流
OutputStream os = socket.getOutputStream();
//3.将低级输出流包装成特殊流传输数据
DataOutputStream dos = new DataOutputStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String msg = sc.nextLine();
if ("exit".equals(msg)) {
System.out.println("退出聊天");
dos.close();
socket.close();
break;
}
dos.writeUTF(msg);
dos.flush(); //将缓冲池中的内容刷新到管道中
}
}
}
接收多个客户端消息
package com.itheima.tcpconnet;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class tcpServerDemo {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信,服务端开发
System.out.println("===服务端启动===");
//1.创建ServerSocket通信管道对象,
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
Socket socket = serverSocket.accept();
System.out.println(socket.getInetAddress().getHostAddress() + "-" + socket.getPort() + "上线了");
new MyThread(socket).start();
}
}
}
class MyThread extends Thread {
private Socket socket;
public MyThread(Socket socket) {
this.socket = socket;
}
//每开连接一个客户端,则开一个线程处理连接
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true) {
String msg = dis.readUTF();
System.out.println("msg:" + msg);
String address = socket.getInetAddress().getHostAddress();
int port = socket.getPort();
System.out.println("客户端的ip=" + address + ", port=" + port);
System.out.println("=======================================");
}
} catch (Exception e) {
System.out.println(socket.getInetAddress().getHostAddress() + "-" + socket.getPort() + "下线了");
}
}
}
client
package com.itheima.tcpconnet;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class tcpClientDemo {
public static void main(String[] args) throws Exception {
//目标:实现TCP通信, 客户端开发
System.out.println("===客户端启动===");
//1.创建通信管道
Socket socket = new Socket("127.0.0.1", 9999);
//2.获取通信管道的字节输出流
OutputStream os = socket.getOutputStream();
//3.将低级输出流包装成特殊流传输数据
DataOutputStream dos = new DataOutputStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String msg = sc.nextLine();
if ("exit".equals(msg)) {
System.out.println("退出聊天");
dos.close();
socket.close();
break;
}
dos.writeUTF(msg);
dos.flush(); //将缓冲池中的内容刷新到管道中
}
}
}
5.综合实例
1.前置准备
1)时间相关的获取方案
package com.itheima.dateTimetest;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.logging.SimpleFormatter;
public class localDateTime {
public static void main(String[] args) {
//目标:掌握Java提供的获取时间的API
//JDK8之前: Date
Date date = new Date();
System.out.println(date);
//将获取的时间对象格式化
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EEE a");
String result = sdf.format(date);
System.out.println(result);
Date date1 = date; //要修改时间的值,只能重新创建Date对象
date1.setHours(12);
System.out.println(date1);
//JDK8之后: LocalDate 年月日 LocalTime 时分秒 LocalDateTime 年月日时分秒
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
System.out.println(now.getYear());
System.out.println(now.getMonth());
System.out.println(now.getDayOfMonth());
LocalDateTime localDateTime = now.plusSeconds(60); //修改之后会返回新的时间对象,将原有的时间对象保护起来,更加安全
System.out.println(localDateTime);
//将获取的时间对象格式化
DateTimeFormatter dmt = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss EEE a");
String result1 = dmt.format(now);
System.out.println(result1);
}
}
为什么JDK8之后要使用新的时间方案
- 原有的Date类,获取时间部分中的具体年月日等子元素很不方便,JDK8之后的方案提供了丰富的获取子元素的组合,比如:
getYear()
、getDayOfMonth()
… - 修改Date类对象的值之后,原本Date类对象的值也会发生相应改变,不利于我们后续对时间进行处理,而JDK8之后,对时间对象进行修改之后会返回一个新的时间对象,从而保护原本的时间对象不发生改变。
2)高效的字符串拼接方案
原因:
- 原有的String拼接方法(直接+号拼接)效率太差:由于String是不可变对象,每次拼接会产生新的对象,将原本的指向改成新对象
package com.itheima.dateTimetest;
public class stringBuilderDemo {
public static void main(String[] args) {
//目标:高效的字符串拼接手段
//1.直接拼接方法:+ 号拼接字符串内容,如果是大量拼接,效率极差
//String的对象是不可变对象, :共享数据的性能可以,但是修改数据性能差
String str = "";
for (int i = 0; i < 10000000; i++) {
str = str + "abc";
}
System.out.println(str);
//2.StringBuilder
StringBuffer sb = new StringBuffer(); //StringBuilder对象是内容可变的容器
for (int i = 0; i < 10000000; i++) {
sb.append(i);
}
System.out.println(sb);
//StringBuilder只是拼接字符串的手段,结果还是要恢复成字符串
String s = sb.toString();
System.out.println(s);
StringBuffer sb1 = new StringBuffer();
sb1.append("abc").append("def").append("ghi"); //支持链式拼接
}
}
3)BigDecimal
用于解决浮点型运算时,出现结果失真的问题
- 由于计算机底层是二进制的计算方式:那么计算小数时存在精度限制,比如0.1无法精确表示为二进制小数,导致舍入误差。
package com.itheima.dateTimetest;
import java.math.BigDecimal;
public class bigDecimalDemo {
public static void main(String[] args) {
//目标:解决浮点型运算过程中出现的失真问题
double a = 0.1;
double b = 0.2;
System.out.println(a + b); //0.3000000000000004 出现失真问题
//1.把小数包装成BigDecimal对象来运算
BigDecimal a1 = new BigDecimal(a);
BigDecimal b1 = new BigDecimal(b);
System.out.println(a1.add(b1));
double result = a1.doubleValue();
System.out.println(result);
//2.优化: 直接调用valueOf方法
BigDecimal a2 = BigDecimal.valueOf(a);
BigDecimal b2 = BigDecimal.valueOf(b);
System.out.println(a2.add(b2));
double result2 = a2.doubleValue();
System.out.println(result2);
System.out.println("=========================");
//除法注意事项
BigDecimal a3 = BigDecimal.valueOf(0.1);
BigDecimal b3 = BigDecimal.valueOf(0.3);
BigDecimal result3 = a3.divide(b3, 2, RoundingMode.HALF_UP); //当出现除不尽的情况时,需要指定保留多少位,并选择舍弃规则:四舍五入
double result4 = result3.doubleValue();
System.out.println(result4);
}
}
2.成品
Server端:
1.Server
package com.javalearn.serverDemo;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class Server {
//定义一个集合容器存储所有登录进来的客户端管道,以便将群发消息给所有管道
//键存储客户端的管道, 值是管道的用户名称
public static Map<Socket, String> onlineSockets = new HashMap<>();
public static void main(String[] args) {
System.out.println("Server starting...");
try {
//1.注册端口
ServerSocket serverSocket = new ServerSocket(Constant.port);
//2.主线程负责接受客户端的连接请求
while (true) {
//3.调用accept方法,获取到客户端的Socket对象
System.out.println("等待客户端的连接....");
Socket socket = serverSocket.accept();
new ServerThread(socket).start(); //将管道交给独立线程处理,以便支持很多客户端同时进行通信
System.out.println("一个客户端连接成功...");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.ServerThread
package com.javalearn.serverDemo;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
public class ServerThread extends Thread {
private Socket socket;
private String name;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//接收的消息可能有很多种: 1.登录消息 2.群聊消息 3.私聊消息
//客户端需要声明协议发送消息
//比如客户端先发1,代表接下来是登录消息
//比如客户端先发2,代表接下来是群聊消息
//先从socket管道中接收客户端发送来的消息类型编号
DataInputStream dis = new DataInputStream(socket.getInputStream());
while (true) {
int type = dis.readInt();
switch (type) {
case 1:
//客户端发来了登录消息,接下来要接收昵称数据,再更新全部在在线客户端的在线人数列表
String nickname = dis.readUTF(); //读取到昵称
this.name = nickname;
//将登录成功的客户端socket存入到在线集合
Server.onlineSockets.put(socket, nickname);
//更新全部客户端的在线人数列表
updateSocketOnLineUserList();
break;
case 2:
//客户端发送了群聊消息,接下来要接收群聊消息内容,将内容发送给全部在线客户端
String msg = dis.readUTF();
sendmsgToAll(msg);
break;
case 3:
break;
}
}
} catch (Exception e) {
System.out.println("客户端" + name + "下线了:" + socket.getInetAddress().getHostAddress());
Server.onlineSockets.remove(socket); //将对应的下线客户端从在线管道中移除
updateSocketOnLineUserList(); //移除之后更新在线人数列表
}
}
//服务端发送给客户端的在线人数列表
private void updateSocketOnLineUserList() {
//更新全部客户端的在线人数列表
//拿到全部在线客户端的用户名称,把这些名称转发给全部在线socket管道
//拿到当前全部在线用户昵称
Collection<String> onLineUsers = Server.onlineSockets.values();
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();
}
}
}
//服务端发送给客户端的群聊消息更新
public void sendmsgToAll(String msg) {
//线程每接收一个消息就需要将消息,则将消息推送给所有在线的客户端
StringBuilder sb = new StringBuilder();
//获取用户昵称
String name = Server.onlineSockets.get(socket); //通过当前客户端的socket拿到客户端的昵称
//获取当前时间
LocalDateTime ldt = LocalDateTime.now(); //获取到当前的时间
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EEE a");
String time = dtf.format(ldt); //格式化时间
//拼接消息
String msgResult = sb.append(name).append(" ").append(time).append("\r\n")
.append(msg).append("\r\n").toString();
//推送给全部客户端
for (Socket socket : Server.onlineSockets.keySet()) {
try {
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(2); //1代表告诉客户端发送的消息是在线人数列表消息,2 代表发的是群聊消息
dos.writeUTF(msgResult);
dos.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
3.Constant
package com.javalearn.serverDemo;
public class Constant {
public static final int port = 6666;
public static final String ip = "127.0.0.1";
}
2.Client端
1.ClientEntryFrame
package com.javalearn.UI;
import javax.swing.*;
import java.awt.*;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class ChatEntryFrame extends JFrame {
private JTextField nicknameField;
private JButton enterButton;
private JButton cancelButton;
private Socket socket; //记住当客户端系统的通信管道
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()) {
//立即发送登录消息给服务端程序
try {
login(nickname); //登录方法
//进入聊天室逻辑: 启动聊天界面
new ClientChatFrame(nickname, socket);
} catch (Exception ex) {
ex.printStackTrace();
}
//进入聊天逻辑
dispose(); // 关闭窗口
} else {
JOptionPane.showMessageDialog(this, "请输入昵称!");
}
});
cancelButton.addActionListener(e -> System.exit(0));
this.setVisible(true);
}
public void login(String name) throws Exception {
//1.创建Socket管道请求与服务端的socket连接
socket = new Socket(Constant.SERVER_IP, Constant.SERVER_PORT);
//2.立即发送消息类型 1 和自己的昵称给服务端
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(1); //标记消息类型
dos.writeUTF(name); //发送昵称
dos.flush(); //刷新
}
}
2.ClientChatFrame
package com.javalearn.UI;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.List;
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("发送");
private Socket socket;
public ClientChatFrame() {
initView();
this.setVisible(true);
}
public ClientChatFrame(String nickname, Socket socket) {
this(); //调用上面构造器,初始化界面信息
//初始话数据
//立马展示昵称到窗口
this.setTitle(nickname + "的聊天界面");
this.socket = socket;
//立即把客户端的socket管道交给一个独立的线程专门负责读取客户端socket从服务端收到的在线人数更新数据或者群聊数据
new ClientThread(socket, this).start();
}
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);
//给发送按钮绑定点击事件
sendBn.addActionListener(e -> {
//获取聊天输入框中的内容
String msg = smsSend.getText();
//清空聊天框
smsSend.setText("");
//发送消息
sendMsgToServer(msg);
});
// 添加组件
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);
}
public static void main(String[] args) {
new ClientChatFrame();
}
public void updateOnlineUsers(String[] onLineNames) {
//将这个线程读取到的在线用户名称展示到界面上
onLineUsers.setListData(onLineNames);
}
//更新群聊消息
public void setMsgToWindow(String msg) {
smsContent.append(msg);
}
//给服务端发消息
private void sendMsgToServer(String msg) {
try {
//从socket管道得到一个特殊数据输出流
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
//把消息发送给服务端
dos.writeInt(2);
dos.writeUTF(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.ClientThread
package com.javalearn.UI;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class ClientThread extends Thread {
private Socket socket;
private DataInputStream dis;
private ClientChatFrame window;
public ClientThread(Socket socket, ClientChatFrame window) {
this.socket = socket;
this.window = window;
}
@Override
public void run() {
try {
//接收的消息可能有很多种: 1.在线人数更新消息 2.群聊消息 3.私聊消息
//客户端需要声明协议发送消息
//比如客户端先发1,代表接下来是登录消息
//比如客户端先发2,代表接下来是群聊消息
//先从socket管道中接收客户端发送来的消息类型编号
dis = new DataInputStream(socket.getInputStream());
while (true) {
int type = dis.readInt();
switch (type) {
case 1:
//服务端发来的在新人数更新消息
updateClientOnLineUserList(dis);
break;
case 2:
//服务端发送来的群聊消息
getMsgToWindow();
break;
case 3:
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
//更新群聊消息
private void getMsgToWindow() throws IOException {
//获取群聊消息
String msg = dis.readUTF();
window.setMsgToWindow(msg);
}
//更新客户端的在线用户列表
private void updateClientOnLineUserList(DataInputStream dis) throws Exception {
//1.读取有多少个在线用户
int count = dis.readInt();
//2.循环控制读取多少个用户信息
String[] names = new String[count];
for (int i = 0; i < count; i++) {
//读取每个用户的信息
String nickName = dis.readUTF();
//将每个用户的信息添加到集合中
names[i] = nickName;
}
//将集合中的数据展示到窗口上
window.updateOnlineUsers(names);
}
}
4.Constant
package com.javalearn.UI;
public class Constant {
public static final String SERVER_IP = "127.0.0.1";
public static final int SERVER_PORT = 6666;
}
5.App
:客户端启动程序
package com.javalearn.UI;
public class App {
public static void main(String[] args) {
new ChatEntryFrame();
}
}
*注上述所有内容来自黑马程序员的B站学习笔记,仅作为学习交流,不作为商业用途,如有侵权,联系删除。