服务端:
package JAVA_API.num18_socket;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 聊天室服务端
*
* @author yyc
* 2021/9/28 14:44
*/
public class Server {
/*
* 运行在服务端的ServerSocket 主要有两个作用:
* 1.向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
* 2.监听服务端口,一旦一个客户端通过改端口建立连接会自动创建一个Socket,服务端就可以通过这个Socket与客户交互了。
*
* 如果我们把Socket比喻为电话,nameServerSocket相当于是某客服中心的总机。
* */
private ServerSocket serverSocket ;
/*
* 创建一个线程安全的集合
* 线程安全的集合中,add,remove 等方法上都直接使用了synchronized修饰,多个线程是不能同时调用一个集合的这些方法的。
* 保证了同步执行
*
* */
//建一个集合用来存放通过流写出给的客户数量,这个集合应是一个线程安全的.
private List<PrintWriter> allOutput = Collections.synchronizedList(new ArrayList<>());
//进行服务端的初始化:
public Server(){
try {
System.out.println("正在启动服务端...");
/*
* 实例化时要指定服务端口,如果该端口被当前系统其他应用程序占用时,会报异常:
* java.net.BindException: Address already in use: JVM_Bind(IDEA端口号:)
* */
//初始化时给成员变量赋值,在()里填入数字申请端口号,用来连接服务器
//IllegalArgumentException: Port value out of range: 65536
//serverSocket = new ServerSocket(65536);
serverSocket = new ServerSocket(5050);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
try {
/*
* ServerSocket提供的方法:
* Socket accept();该方法是一个阻塞方法,调用后程序进入阻塞状态,直到一个客户端实例化Socket与当前服务端建立连接,
* 此时accept()方法会返回一个Socket实例,服务端通过他就可以与客户端交互了。
*
* 可以理解为这个动作相当于是总机的"接电话"操作 。
*
* */
while (true) {
System.out.println("等待客户端连接...");
//然后在main方法里调用方法,先启服务端,在启动客户端,accept()获取到的socket的返回值是连接当前服务端的客户端的socket。客户端的socket是Output来的。
Socket socket = serverSocket.accept();//用来监听
System.out.println("一个客户端连接了...");
//启动一个线程来负责与客户端交互的操作:
//1.创建线程任务,传入的参数就是监听到的Socket,
ClientHandler handler = new ClientHandler(socket);
//2.创建一个线程并执行该任务
Thread thread1 = new Thread(handler);
//3.启动线程
thread1.start();
}
} catch(IOException e){
e.printStackTrace();
}
}
/*
* 该线程任务用于指定的客户端进行交互。
* 每个连接服务端的客户端都是通过该线程进行交互的。
* 即:一个客户端靠一个线程进行交互
* */
//写一个内部类来定义任务
private class ClientHandler implements Runnable {
//内部类可以有成员方法和变量
//声明一个成员变量和构造方法
private Socket socket;
private String host;//当前线程处理的客户端的地址信息
//通过传入的socket获取客户端的信息。
public ClientHandler(Socket socket){
this.socket = socket;
//通过socket获取远端计算机地址信息(对于服务器而言,远端就是客户端),地址可以用来拼上谁谁说的话
//获取客户端地址就是start()方法中监听到的socket即是客户端ip及端口,
host = socket.getInetAddress().getHostAddress();
}
@Override
public void run() {
//将pw放在这里是为了能在finally中看到并处理
PrintWriter pw = null;
//socket能获取的只有字节流
try {
/*
* Socket提供的方法:
* InputStream getInputStream();
* 通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
*
* 这里相当于读取当前服务端中这个Socket对应的远端(客户端)
* 那边Socket获取的输出流写出的字节数据
* */
InputStream inputstream = socket.getInputStream();
//想要读,先转换
InputStreamReader isr = new InputStreamReader(inputstream,"UTF-8");
BufferedReader br = new BufferedReader(isr);
//用这个流来写出数据给客户端
pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")),true);
/*
* 将该消息存入共享集合allOut中,便于其他ClientHandler将消息发送给当前客户端。
* 由于是线程安全的集合,不必自行添加锁机制即可保证线程同步调用
* */
allOutput.add(pw);
//host是客户地址,allOutput.size()客户上线人数
System.out.println(host + "上线了,当前的在线人数:" + allOutput.size());
//调用发送消息的方法把消息发送给所有客户
sendMessage(host + "上线了,当前的在线人数:" + allOutput.size());
String line ;
while((line = br.readLine()) != null){
//host是读到的客户端地址,读到的内容是line
System.out.println(host + "说:" + line);
//将消息回复给所有的客户端,想要实现群发的效果,可以把写出到这一句放到集合,需要把客户端放到集合里
//pw.println(line);不能实现群发
//将客户输入的信息发给所有用户
sendMessage(host + "说:" + line);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
//处理客户端断开后的操作
//1.从集合中移除
allOutput.remove(pw);
//移除之后给一个下线的通知
//host是客户地址,allOutput.size()客户上线人数
//2.广播下线通知
System.out.println(host + "下线了,当前的在线人数:" + allOutput.size());
try {
//3.关闭socket释放资源
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//发送消息的方法
/**
* 将消息转发给所有客户端(发送给集合里的每一个人,遍历集合)
* @pararm message 要发送的内容
* */
private void sendMessage(String message){
/**
* 遍历集合的每一个输出流,把消息发送出去(给所有客户端)
* */
allOutput.forEach(pw -> pw.println(message));
/* for (int i = 0 ; i < allOutput.size(); i++){
allOutput.get(i).println(message);
}*/
}
}
//main方法用于测试
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
客户端:
package JAVA_API.num18_socket;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* 聊天室客户端
* 想要实现网络通信需要一个Socket套接字(Socket是一个应用程序接口(应用程序接口又叫API,即Socket是一个interface提供了一个方法使用))
* 有了这样一种接口技术,一台计算机就可以通过软件的方式与任何一台具有Socket接口的计算机进行通信。端口在计算机编程上也就是"Socket接口"。
* @author yyc
* 2021/9/28 14:36
*/
public class Client {
/*
TCP和UDP协议:
TCP(Transmission Control Protocol):安全性可靠性更高
UDP协议:不安全,实时性。
* java.util.Socket套接字(客户端套接字)
* Socket封装了TCP协议的通讯细节,使得我们使用它可以 与服务端建立网络连接,并通过他获取两个流(一个输入一个输出),
* 然后使用这两个流的读写操作完成与服务端的数据交互。
* */
private Socket socket;
/**/
public Client(){
/*
* 通过Win + ipconfig 查本机host
* 实例化Socket时通常需要传入两个参数:
* public Socket(String host, int port)
* 参数1为服务端口的地址信息(IP地址,如果连本机可用loacalhost)
* 参数2为服务端打开的服务端口,即:服务端ServerSocket申请的端口
* */
try {
System.out.println("正在连接服务器...");
socket = new Socket("192.168.43.196",5050);
System.out.println("成功连接服务器!");
} catch (IOException e) {
e.printStackTrace();
}
}
/*
* 客户端开始工作的 方法:用户发消息客户端与服务器做交互
* Socket提供输入流供我们输入
* */
public void start(){
//在写之前也需要
//客户端启动后,先启动一个线程来读取服务端发送过来的消息
ServerHandler handler = new ServerHandler();
Thread thread = new Thread(handler);
thread.start();
/*
Socket 提供的方法:
OutputStream getOutputStream();该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络法送给对方。
*/
//把流放在try()
try(PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")),true);
){
//定义一个输出流用于写出数据到
//OutputStream outputStream = socket.getOutputStream();
//PrintWriter按行写出,BufferedWriter缓冲字符输出流加快写出数据,OutputStreamWriter将字符转换成字节,
System.out.println("开始聊天吧!单独输入exit是退出!");
Scanner scanner = new Scanner(System.in);
while(true){
String line = scanner.nextLine();
if ("exit".equals(line)){
System.out.println("聊天结束");
break;
}
//将客户端控制台输入的信息发送到服务端,通过pw.Println()写出到服务端
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
socket.close();//最终和对方(服务器)断开连接
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 该线程用来负责循环接收服务端发送过来的消息
* */
private class ServerHandler implements Runnable {
@Override
public void run(){
try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"))){
//用来接收流读取出来的服务器发送过来的信息
String message;
//读取服务器发送过来的一行字符串
while((message = br.readLine()) != null){
System.out.println(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
//测试
public static void main(String[] args) {
/*
* java.net.ConnectException: Connection refused: connect
* 拒接链接,因为此时服务端还没有接受客户端连接
* */
Client client = new Client();
client.start();
}
}