java网络编程《聊天室》
- ---------客户端《Client.java》
- ---------服务器《Server.java》
1、知识点:
00.网络基本概念:
(1)七层模型:物理型 数据链路层 网络层 传输层(UDP/TCP) 会话层 应用层 表示层
后四层与Java相关(HTTP,网页相关)(FPT,小范围服务协议)(POP3,邮箱邮件还有SMTP)
(2)IP:在网络中标记主机.IPV4(四组数来表示IP地址,每一组数取值范围0~255 ),后主推IPV6
(3)端口:计算机与外界交互的媒介-----端口号,0~65535
(4)域名:各个网站提供的便于记忆的标记”baidu(二级域名).com(一级域名)”从后往前看//com(商业网站)org(政府网站)edu(教育类网站)
(5)DNS:解析服务器:将域名和IP地址进行对应的
(6)UPD:基于流来使用的,不建立连接,不可靠传输,需要对数据进行封包每个包不超过64K.适用于
对速度依赖性比较强但是对可靠性比较低的场景
(8)TCP:基于流的,建立连接,经历三次握手,初次客户端发给服务端,请求建立连接/发送数据,二次
服务端发给客户端,确认可以建立链接/发送数据,第三次客户端发给服务端,再次确认,收
到信息,准备链接/发送,可靠但传输速率相对较慢,理论上不限制传输数据的大小,适用于
对可靠性的依赖性更高却对速度依赖性较低的场景------文件传输
01.Socket通信模型:
02.什么是Socket?
(1)java.net.Socket
java.net.Socket:————>Socket:插座 套接字
Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
【Socket(套接字),用来描述IP地址和端口,是通信链的句柄,应用程序可以通过Socket向网络发送请求或者应答网络请求!
Socket是支持TCP/IP协议的网络通信的基本操作单元,是对网络通信过程中端点的抽象表示。
包含了进行网络通信所必须的五种信息:
1)连接所使用的协议;
2)本地主机的IP地址;
3)本地远程的协议端口;
4)远地主机的IP地址
5)远地进程的协议端口】
1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化
Socket的过程就是与服务端建立连接的过程。
参数1:服务端的IP地址
参数2:服务端开启的服务端口
我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
2、
01.Socket提供了两个重要的方法:
(1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
(2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
02.Socket提供了:
(1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
(2).getInetAddress().getHostAddress:获取远端客户端的地址信息
03.什么是ServerSocket?
-----------【如果我们把Socket比喻为电话,那么ServerSocket相当于是某客服中心的总机。
】
java.net.ServerSocket:
1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
(1)向服务端申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
(2)监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。
2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
java.net.BindException:address already in use
绑定异常:地址被使用了
3、ServerSocket的accept()方法:——>是一个阻塞方法;
开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法会立即返回一个
Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
注:阻塞方法:调用后,程序就"卡住"不往下执行了。
4、防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端——————>此时会报异常:
{对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法会抛出异常:
java.net.SocketException: connection reset--->服务端无法避免这个异常。}
(1)解决办法:(处理客户端断开连接后的操作):————>即:将pw从数组allOut中删除(数组缩容):
2、聊天室10个版本目标:
00.v0:—> 大致步骤 和 相关知识 <—
(1)客户端:
(1)客户端:Socket通信实现步骤解析:
- Step 1:创建Socket对象
- Step 2:发起链接,绑定链接地址
- Step 3:获取自带的输出流,写出数据,禁用输出流
- Step 4:如果服务端有打回的数据,则需要获取输入流读取数据,禁用输入流
- Step 5:关闭输入输出流,以及Socket
java.net.Socket:————>Socket:插座 套接字
Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
参数1:服务端的IP地址
参数2:服务端开启的服务端口
我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
2、
01.Socket提供了两个重要的方法:
(1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
(2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
02.Socket提供了:
(1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
(2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
3、String提供方法:equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
4、创建类ServerHandler 并implements Runnable
(2)服务端:
(2)服务端:ServerSocket通信实现步骤解析:
- Step 1:创建服务器端套接字对象,并且绑定监听的端口号
- Step 2:接受连接,获取到一个Socket对象
- Step 3:获取输入流,读取消息,禁用输入流
- Step 4:如果需要向客户端打回消息,则需要获取输出流写出数据,禁用输出流
- Step 5:关闭输入输出流
ServerSocket相当于"总机"
1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
(1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
(2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
java.net.BindException:address already in use
绑定异常:地址被使用了
3、ServerSocket的accept()方法:
是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
阻塞方法:调用后,程序就"卡住"不往下执行了。
4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
5、定义一个PrintWriter类型数组allOut进行扩容将pw放到数组最后一个元素。实现:遍历allOut数组,将消息发送所有给客户端
6、防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端——————>此时会报异常:
{对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法会抛出异常:
java.net.SocketException: connection reset--->服务端无法避免这个异常。}
(1)解决办法:(处理客户端断开连接后的操作):————>即:将pw从数组allOut中删除(数组缩容):
7、选取合适的锁对象:-------外部类名.this(内部类访问外部类对象)
(1)写一个同步块————>synchronized (Server.this) { 扩容、放在最后一个元素}
(2)写一个同步块————>synchronized (Server.this) { for(){if(){缩容、放在最后一个元素}}}
(3)将【广播消息给所有客户端】中的也放在同步块里:synchronized (Server.this) {打印语句、for}
01. v1:与服务端建立连接
java.net.Socket
:Socket(套接字)封装了TCP协议的通讯细节,是的我们使用它可以与服务端建立网络链接,并通过 它获取两个流(一个输入一个输出),然后使用这两个流的读写操作完成与服务端的数据交互
java.net.ServerSocket:
:ServerSocket运行在服务端,作用有两个:
(1)向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
(2)监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。
如果我们把Socket比喻为电话,那么ServerSocket相当于是某客服中心的总机。
(1)客户端:
package apiday.day07.socket.v1;
import java.io.IOException;
import java.net.Socket;
public class Client {
/*
java.net.Socket 套接字
Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,
并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成
与远端计算机的数据交互工作。
我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克
风(输出流),通过它们就可以与对方交流了。
*/
private Socket socket;
/**
* 构造方法,用来初始化客户端
*/
public Client(){
try {
System.out.println("正在链接服务端...");
/*
实例化Socket时要传入两个参数
参数1:服务端的地址信息
可以是IP地址,如果链接本机可以写"localhost"
参数2:服务端开启的服务端口
我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上
的服务端应用程序。
实例化的过程就是链接的过程,如果链接失败会抛出异常:
java.net.ConnectException: Connection refused: connect
*/
socket = new Socket("localhost",8088);
System.out.println("与服务端建立链接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start(){
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
(2)服务端:
package apiday.day07.socket.v1;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* java网络编程:
* java.net.Socket:Socket(套接字)封装了TCP协议的通讯细节,是的我们使用它可以与服务端建立网络链接,
* 并通过它获取两个流(一个输入一个输出),然后使用这两个流的读写操作完成与服务端的数据交互
*
* java.net.ServerSocket
* ServerSocket运行在服务端,作用有两个:
* 1:向系统申请服务端口,客户端的Socket就是通过这个端口与服务端建立连接的。
* 2:监听服务端口,一旦一个客户端通过该端口建立连接则会自动创建一个Socket,并通过该Socket与客户端进行数据交互。
*
* 如果我们把Socket比喻为电话,那么ServerSocket相当于是某客服中心的总机。
*
*/
public class Server {
/**
* 运行在服务端的ServerSocket主要完成两个工作:
* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接
* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket
* 就可以和该客户端交互了
*
* 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个
* 电话使得服务端与你沟通。
*/
private ServerSocket serverSocket;
/**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
/*
实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他
应用程序占用的端口相同,否则会抛出异常:
java.net.BindException:address already in use
端口是一个数字,取值范围:0-65535之间。
6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
System.out.println("等待客户端链接...");
/*
ServerSocket提供了接受客户端链接的方法:
Socket accept()
这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例
通过这个Socket就可以与客户端进行交互了。
可以理解为此操作是接电话,电话没响时就一直等。
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
02. v2:客户端与服务端完成第一次通讯(发送一行字符串)
Socket提供了两个重要的方法:
OutputStream getOutputStream():
该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
InputStream getInputStream():
通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
(1)客户端:
package apiday.day07.socket.v2;
import java.io.*;
import java.net.Socket;
public class Client {
/*
java.net.Socket 套接字
Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,
并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成
与远端计算机的数据交互工作。
我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克
风(输出流),通过它们就可以与对方交流了。
*/
private Socket socket;
/**
* 构造方法,用来初始化客户端
*/
public Client(){
try {
System.out.println("正在链接服务端...");
/*
实例化Socket时要传入两个参数
参数1:服务端的地址信息
可以是IP地址,如果链接本机可以写"localhost"
参数2:服务端开启的服务端口
我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上
的服务端应用程序。
实例化的过程就是链接的过程,如果链接失败会抛出异常:
java.net.ConnectException: Connection refused: connect
*/
socket = new Socket("localhost",8088);
System.out.println("与服务端建立链接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start(){
try {
/*
Socket提供了一个方法:
OutputStream getOutputStream()
该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。
*/
//低级流,将字节通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
//高级流,负责块写文本数据加速
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串,自动行刷新
PrintWriter pw = new PrintWriter(bw,true);
pw.println("你好服务端!");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
(2)服务端:
package apiday.day07.socket.v2;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 聊天室服务端
*/
public class Server {
/**
* 运行在服务端的ServerSocket主要完成两个工作:
* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接
* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket
* 就可以和该客户端交互了
*
* 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个
* 电话使得服务端与你沟通。
*/
private ServerSocket serverSocket;
/**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
/*
实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他
应用程序占用的端口相同,否则会抛出异常:
java.net.BindException:address already in use
端口是一个数字,取值范围:0-65535之间。
6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
System.out.println("等待客户端链接...");
/*
ServerSocket提供了接受客户端链接的方法:
Socket accept()
这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例
通过这个Socket就可以与客户端进行交互了。
可以理解为此操作是接电话,电话没响时就一直等。
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
/*
Socket提供的方法:
InputStream getInputStream()
获取的字节输入流读取的是对方计算机发送过来的字节
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"UTF-8");
BufferedReader br = new BufferedReader(isr);
String message = br.readLine();
System.out.println("客户端说:"+message);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
03. v3:只能收到第一个客户端的消息
(1)客户端:
package apiday.day07.socket.v3;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* 聊天室客户端
*/
public class Client {
/*
java.net.Socket 套接字
Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,
并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成
与远端计算机的数据交互工作。
我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克
风(输出流),通过它们就可以与对方交流了。
*/
private Socket socket;
/**
* 构造方法,用来初始化客户端
*/
public Client(){
try {
System.out.println("正在链接服务端...");
/*
实例化Socket时要传入两个参数
参数1:服务端的地址信息
可以是IP地址,如果链接本机可以写"localhost"
参数2:服务端开启的服务端口
我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上
的服务端应用程序。
实例化的过程就是链接的过程,如果链接失败会抛出异常:
java.net.ConnectException: Connection refused: connect
*/
socket = new Socket("localhost",8088);
System.out.println("与服务端建立链接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start(){
try {
/*
Socket提供了一个方法:
OutputStream getOutputStream()
该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。
*/
//低级流,将字节通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
//高级流,负责块写文本数据加速
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串,自动行刷新
PrintWriter pw = new PrintWriter(bw,true);
Scanner scanner = new Scanner(System.in);
while(true) {
String line = scanner.nextLine();
if("exit".equalsIgnoreCase(line)){
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
/*
通讯完毕后调用socket的close方法。
该方法会给对方发送断开信号。
*/
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
(2)服务端:
package apiday.day07.socket.v3;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 聊天室服务端
*/
public class Server {
/**
* 运行在服务端的ServerSocket主要完成两个工作:
* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接
* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket
* 就可以和该客户端交互了
*
* 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个
* 电话使得服务端与你沟通。
*/
private ServerSocket serverSocket;
/**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
/*
实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他
应用程序占用的端口相同,否则会抛出异常:
java.net.BindException:address already in use
端口是一个数字,取值范围:0-65535之间。
6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
System.out.println("等待客户端链接...");
/*
ServerSocket提供了接受客户端链接的方法:
Socket accept()
这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例
通过这个Socket就可以与客户端进行交互了。
可以理解为此操作是接电话,电话没响时就一直等。
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
/*
Socket提供的方法:
InputStream getInputStream()
获取的字节输入流读取的是对方计算机发送过来的字节
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,"UTF-8");
BufferedReader br = new BufferedReader(isr);
String message = null;
while((message = br.readLine())!=null) {
System.out.println("客户端说:" + message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
04. v4:还是只能接收第一个客户端消息
需要注意的几个点:
(1)当客户端不再与服务端通讯时,需要调用socket.close()断开链接,此时会发送断开链接的信号给服务端。这时服务端的br.readLine()方法会返回null,表示客户端断开了链接。
(2)当客户端链接后不输入信息发送给服务端时,服务端的br.readLine()方法是出于阻塞状态的,直到读取了一行来自客户端发送的字符串。
多客户端链接:之前只有第一个连接的客户端可以与服务端说话。
原因:服务端只调用过一次accept方法,因此只有第一个客户端链接时服务端接受了链接并返回了
Socket,此时可以与其交互。
添加循环操作后,发现依然无法实现:
原因在于:
外层的while循环里面嵌套了一个内层循环(循环读取客户端发送消息),而循环执行机制决定了
里层循环不结束,外层循环则无法进入第二次操作。
(1)客户端:
package apiday.day07.socket.v4;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* 聊天室客户端
*/
public class Client {
/*
java.net.Socket 套接字
Socket封装了TCP协议的通讯细节,我们通过它可以与远端计算机建立链接,
并通过它获取两个流(一个输入,一个输出),然后对两个流的数据读写完成
与远端计算机的数据交互工作。
我们可以把Socket想象成是一个电话,电话有一个听筒(输入流),一个麦克
风(输出流),通过它们就可以与对方交流了。
*/
private Socket socket;
/**
* 构造方法,用来初始化客户端
*/
public Client(){
try {
System.out.println("正在链接服务端...");
/*
实例化Socket时要传入两个参数
参数1:服务端的地址信息
可以是IP地址,如果链接本机可以写"localhost"
参数2:服务端开启的服务端口
我们通过IP找到网络上的服务端计算机,通过端口链接运行在该机器上
的服务端应用程序。
实例化的过程就是链接的过程,如果链接失败会抛出异常:
java.net.ConnectException: Connection refused: connect
*/
socket = new Socket("localhost",8088);
System.out.println("与服务端建立链接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start(){
try {
/*
Socket提供了一个方法:
OutputStream getOutputStream()
该方法获取的字节输出流写出的字节会通过网络发送给对方计算机。
*/
//低级流,将字节通过网络发送给对方
OutputStream out = socket.getOutputStream();
//高级流,负责衔接字节流与字符流,并将写出的字符按指定字符集转字节
OutputStreamWriter osw = new OutputStreamWriter(out,"UTF-8");
//高级流,负责块写文本数据加速
BufferedWriter bw = new BufferedWriter(osw);
//高级流,负责按行写出字符串,自动行刷新
PrintWriter pw = new PrintWriter(bw,true);
Scanner scanner = new Scanner(System.in);
while(true) {
String line = scanner.nextLine();
if("exit".equalsIgnoreCase(line)){
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
/*
通讯完毕后调用socket的close方法。
该方法会给对方发送断开信号。
*/
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
apiday.day07.socket.v3.Client client = new apiday.day07.socket.v3.Client();
client.start();
}
}
(2)服务端:
package apiday.day07.socket.v4;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 聊天室服务端
*/
public class Server {
/**
* 运行在服务端的ServerSocket主要完成两个工作:
* 1:向服务端操作系统申请服务端口,客户端就是通过这个端口与ServerSocket建立链接
* 2:监听端口,一旦一个客户端建立链接,会立即返回一个Socket。通过这个Socket
* 就可以和该客户端交互了
*
* 我们可以把ServerSocket想象成某客服的"总机"。用户打电话到总机,总机分配一个
* 电话使得服务端与你沟通。
*/
private ServerSocket serverSocket;
/**
* 服务端构造方法,用来初始化
*/
public Server(){
try {
System.out.println("正在启动服务端...");
/*
实例化ServerSocket时要指定服务端口,该端口不能与操作系统其他
应用程序占用的端口相同,否则会抛出异常:
java.net.BindException:address already in use
端口是一个数字,取值范围:0-65535之间。
6000之前的的端口不要使用,密集绑定系统应用和流行应用程序。
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端开始工作的方法
*/
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
/*
ServerSocket提供了接受客户端链接的方法:
Socket accept()
这个方法是一个阻塞方法,调用后方法"卡住",此时开始等待客户端
的链接,直到一个客户端链接,此时该方法会立即返回一个Socket实例
通过这个Socket就可以与客户端进行交互了。
可以理解为此操作是接电话,电话没响时就一直等。
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
/*
Socket提供的方法:
InputStream getInputStream()
获取的字节输入流读取的是对方计算机发送过来的字节
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String message = null;
while ((message = br.readLine()) != null) {
System.out.println("客户端说:" + message);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
05. v5:可接收多客户端发的消息
————>使用多线程实现多客户端连接服务端
server:使用多线程实现多客户端连接服务端
①该线程任务是用一个线程来处理一个客户端的交互:
定义一个ClientHandler类重写run()方法:
②并获取远端客户端的地址信息
(1)客户端:
package apiday.socket.v5;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 聊天室客户端Client:
* java.net.Socket:————>Socket:插座 套接字
* Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
* 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
*
* 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
*
* 2、
* 01.Socket提供了两个重要的方法:
* (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
* (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
*
* 02.Socket提供了:
* (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
* (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
*
*
* 4、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
*/
public class Client {
/*
java.net.Socket 插座 套接字
Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
读写完成数据交换。
*/
private Socket socket;
/**
* 构造方法:用于初始化客户端
*/
public Client() {
try {
/**
* 实例化Socket时常用的构造方法:Socket(String host,int port)
* 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
* 该机器上的服务端应用程序从而与之建立连接。
*/
//本机IP地址的写法可以是“localhost”
System.out.println("正在连接服务端");
socket = new Socket("192.168.0.104", 8088);
System.out.println("与服务端连接了");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
/* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
//是一个简易记事本功能。控制台输入的每行字符串都按行写入文件
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
//equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
if ("exit".equalsIgnoreCase(line)) {
break;
}
pw.println(line);//自动行刷新,调用println()方法
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//Socket提供了.close()方法:可以与远端计算机断开连接。
// 该方法调用时,也会自动关闭通过他获取的输入流和输出流。
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
(2)服务端:
package apiday.socket.v5;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
* 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
* (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
* (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*
* 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
* 如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
* java.net.BindException:address already in use
* 绑定异常:地址被使用了
*
* 3、ServerSocket的accept()方法:
* 是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
* 会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
* 阻塞方法:调用后,程序就"卡住"不往下执行了。
*
* 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
*
*/
public class Server {
/*
java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
(1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
(2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*/
private ServerSocket serverSocket;
public Server() {
try {
/*
ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
端口建立连接。
如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
java.net.BindException:address already in use
绑定异常:地址被使用了
*/
System.out.println("正在启动服务端");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
try {
/*
ServerSocket的accept方法是一个阻塞方法。
开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
相当于是接电话的动作。
阻塞方法:调用后,程序就"卡住"不往下执行了。
*/
while (true) {
System.out.println("等待客户端连接");
Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
System.out.println("一个客户端连接了!");
//启动一个线程来处理该客户端的交互: Handler:处理器
//①创建线程任务
ClientHandler clientHandler = new ClientHandler(socket);
//②创建1条线程执行该任务
Thread thread = new Thread(clientHandler);
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
private class ClientHandler implements Runnable{
private Socket socket;
//记录远端计算机的地址信息:
private String host;
public ClientHandler(Socket socket){
this.socket = socket;
host = socket.getInetAddress().getHostAddress();
}
public void run() {
try {
//通过socket获取输入流读取对方发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
/*
阻塞:
这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
处于阻塞状态,直到对方发送过来一行字符串为止。
如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
*/
String line;
while ((line = br.readLine())!=null){
System.out.println(host+"说:"+line);
}
} catch (IOException e) {
//e.printStackTrace();
} finally {
}
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
06. v6:客户端发给服务端的消息能在客户端显示
————>初步实现服务端发送消息给客户端:
(1)客户端:
package apiday.socket.v6;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 聊天室客户端Client:
* java.net.Socket:————>Socket:插座 套接字
* Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
* 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
*
* 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
*
* 2、
* 01.Socket提供了两个重要的方法:
* (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
* (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
*
* 02.Socket提供了:
* (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
* (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
*
*
* 3、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
*/
public class Client {
/*
java.net.Socket 插座 套接字
Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
读写完成数据交换。
*/
private Socket socket;
/**
* 构造方法:用于初始化客户端
*/
public Client() {
try {
/**
* 实例化Socket时常用的构造方法:Socket(String host,int port)
* 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
* 该机器上的服务端应用程序从而与之建立连接。
*/
//本机IP地址的写法可以是“localhost”
System.out.println("正在连接服务端");
socket = new Socket("192.168.0.104", 8088);
System.out.println("与服务端连接了");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
/* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
//通过socket获取输入流读取服务端发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//是一个简易记事本功能。控制台输入的每行字符串都按行写入文件
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
//equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
if ("exit".equalsIgnoreCase(line)) {
break;
}
pw.println(line);//自动行刷新,调用println()方法
//读取服务端发送过来的一行字符串:
line = br.readLine();
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//Socket提供了.close()方法:可以与远端计算机断开连接。
// 该方法调用时,也会自动关闭通过他获取的输入流和输出流。
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
(2)服务端:
package apiday.socket.v6;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.sql.Struct;
/**
* 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
* 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
* (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
* (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*
* 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
* 如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
* java.net.BindException:address already in use
* 绑定异常:地址被使用了
*
* 3、ServerSocket的accept()方法:
* 是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
* 会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
* 阻塞方法:调用后,程序就"卡住"不往下执行了。
*
* 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
*
*/
public class Server {
/*
java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
(1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
(2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*/
private ServerSocket serverSocket;
public Server() {
try {
/*
ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
端口建立连接。
如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
java.net.BindException:address already in use
绑定异常:地址被使用了
*/
System.out.println("正在启动服务端");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
try {
/*
ServerSocket的accept方法是一个阻塞方法。
开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
相当于是接电话的动作。
阻塞方法:调用后,程序就"卡住"不往下执行了。
*/
while (true) {
System.out.println("等待客户端连接");
Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
System.out.println("一个客户端连接了!");
//启动一个线程来处理该客户端的交互: Handler:处理器
//①创建线程任务
ClientHandler clientHandler = new ClientHandler(socket);
//②创建1条线程执行该任务
Thread thread = new Thread(clientHandler);
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
private class ClientHandler implements Runnable{
private Socket socket;
//记录远端计算机的地址信息:
private String host;
public ClientHandler(Socket socket){
this.socket = socket;
host = socket.getInetAddress().getHostAddress();
}
public void run() {
try {
//通过socket获取输入流读取对方发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//通过socket获取输出流用于给对方发消息:
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
/*
阻塞:
这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
处于阻塞状态,直到对方发送过来一行字符串为止。
如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
*/
String line;
while ((line = br.readLine())!=null){
System.out.println(host+"说:"+line);
//将消息发送给客户端
pw.println(host+"说:"+line);
}
} catch (IOException e) {
//e.printStackTrace();
} finally {
}
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
07. v7:多个客户端之间收发消息有冲突:
——>服务端转发客户端发送的消息给所有客户端,但多个客户端之间收发消息有冲突:
当一个客户端发送一个消息后,服务端收到后如何转发给所有客户端?
问题:例如红色的线程一收到客户端消息后如何获取到橙色的线程二中的输出流?得不到就无法将消息转发给橙色的客户端(进一步延伸就是无法转发给所有其他客户端)
解决:
内部类可以访问外部类的成员,因此在Server中定义一个数组allOut可以被所有内部类ClientHandler实例访问.
从而将这些ClientHandler实例之间想互访的数据存放在这个数组中达到共享数据的目的.
对此只需要将所有ClientHandler中的输出流都存入到数组allOut中就可以达到互访输出流转发消息的目的了.
server:
(1)定义一个PrintWriter[]类型数组 allOut:
在run()中将该输出流存入共享数组allOut中:
1)扩容
2)将pw放到数组最后一个元素
(2)把【将消息发送给客户端】改为————>遍历allOut数组,将消息发送所有给客户端
(1)客户端:
package apiday.socket.v7;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 聊天室客户端Client:
* java.net.Socket:————>Socket:插座 套接字
* Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
* 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
*
* 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
*
* 2、
* 01.Socket提供了两个重要的方法:
* (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
* (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
*
* 02.Socket提供了:
* (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
* (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
*
*
* 3、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
*/
public class Client {
/*
java.net.Socket 插座 套接字
Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
读写完成数据交换。
*/
private Socket socket;
/**
* 构造方法:用于初始化客户端
*/
public Client() {
try {
/**
* 实例化Socket时常用的构造方法:Socket(String host,int port)
* 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
* 该机器上的服务端应用程序从而与之建立连接。
*/
//本机IP地址的写法可以是“localhost”
System.out.println("正在连接服务端");
socket = new Socket("192.168.0.104", 8088);
System.out.println("与服务端连接了");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
/* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
//通过socket获取输入流读取服务端发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//是一个简易记事本功能。控制台输入的每行字符串都按行写入文件
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
//equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
if ("exit".equalsIgnoreCase(line)) {
break;
}
pw.println(line);//自动行刷新,调用println()方法
//读取服务端发送过来的一行字符串:
line = br.readLine();
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//Socket提供了.close()方法:可以与远端计算机断开连接。
// 该方法调用时,也会自动关闭通过他获取的输入流和输出流。
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
(2)服务端:
package apiday.socket.v7;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
* 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
* (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
* (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*
* 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
* 如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
* java.net.BindException:address already in use
* 绑定异常:地址被使用了
*
* 3、ServerSocket的accept()方法:
* 是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
* 会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
* 阻塞方法:调用后,程序就"卡住"不往下执行了。
*
* 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
*
* 5、定义一个PrintWriter类型数组allOut进行扩容将pw放到数组最后一个元素。实现:遍历allOut数组,将消息发送所有给客户端
*/
public class Server {
/*
java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
(1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
(2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*/
private ServerSocket serverSocket;
private PrintWriter[] allOut = {};
public Server() {
try {
/*
ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
端口建立连接。
如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
java.net.BindException:address already in use
绑定异常:地址被使用了
*/
System.out.println("正在启动服务端");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
try {
/*
ServerSocket的accept方法是一个阻塞方法。
开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
相当于是接电话的动作。
阻塞方法:调用后,程序就"卡住"不往下执行了。
*/
while (true) {
System.out.println("等待客户端连接");
Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
System.out.println("一个客户端连接了!");
//启动一个线程来处理该客户端的交互: Handler:处理器
//①创建线程任务
ClientHandler clientHandler = new ClientHandler(socket);
//②创建1条线程执行该任务
Thread thread = new Thread(clientHandler);
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
private class ClientHandler implements Runnable{
private Socket socket;
//记录远端计算机的地址信息:
private String host;
public ClientHandler(Socket socket){
this.socket = socket;
host = socket.getInetAddress().getHostAddress();
}
public void run() {
try {
//通过socket获取输入流读取对方发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//通过socket获取输出流用于给对方发消息:
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
//将该输出流存入共享数组allOut中:
//(1)扩容:
allOut = Arrays.copyOf(allOut,allOut.length+1);
//(2)将pw放到数组最后一个元素:
allOut[allOut.length-1] = pw;
/*
阻塞:
这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
处于阻塞状态,直到对方发送过来一行字符串为止。
如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
*/
String line;
while ((line = br.readLine())!=null){
System.out.println(host+"说:"+line);
//遍历allOut数组,将消息发送所以给客户端:
for(int i=0;i<allOut.length;i++) {
allOut[i].println(host + "说:" + line);
}
}
} catch (IOException e) {
//e.printStackTrace();
} finally {
}
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
08. v8:客户端解决收发消息的冲突问题:
由于客户端start方法中循环进行的操作顺序是:先通过控制台输入一句话后将其发送给服务端,然后再读取服务端发送回来的一句话.
这导致如果客户端不输入内容就无法收到服务端发送过来的其他信息(其他客户端的聊天内容).
因此要将客户端中接收消息的工作移动到一个单独的线程上执行,才能保证收发消息互不打扰
Client:
(1)该线程任务负责处理服务端发送过来的消息:
创建类ServerHandler 并implements Runnable
(2)将上面的【通过socket获取输入流读取服务端发送过来的消息】代码剪切到run()中
(3)在上眠的代码中写:循环读取服务端发送过来的每一行字符串:
(4)将【读取服务端发送过来的一行字符串:】代码删除
(5)启动用于读取服务端发送过来消息的线程
(1)客户端:
package apiday.socket.v8;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 聊天室客户端Client:
* java.net.Socket:————>Socket:插座 套接字
* Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
* 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
*
* 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
*
* 2、
* 01.Socket提供了两个重要的方法:
* (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
* (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
*
* 02.Socket提供了:
* (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
* (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
*
*
* 3、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
*
* 4、创建类ServerHandler 并implements Runnable
*/
public class Client {
/*
java.net.Socket 插座 套接字
Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
读写完成数据交换。
*/
private Socket socket;
/**
* 构造方法:用于初始化客户端
*/
public Client() {
try {
/**
* 实例化Socket时常用的构造方法:Socket(String host,int port)
* 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
* 该机器上的服务端应用程序从而与之建立连接。
*/
//本机IP地址的写法可以是“localhost”
System.out.println("正在连接服务端");
socket = new Socket("192.168.0.104", 8088);
System.out.println("与服务端连接了");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
//启动用于读取服务端发送过来消息的线程:
ServerHandler handler = new ServerHandler();
Thread t = new Thread(handler);
t.start();
/* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
//是一个简易记事本功能。控制台输入的每行字符串都按行写入文件
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
//equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
if ("exit".equalsIgnoreCase(line)) {
break;
}
pw.println(line);//自动行刷新,调用println()方法
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//Socket提供了.close()方法:可以与远端计算机断开连接。
// 该方法调用时,也会自动关闭通过他获取的输入流和输出流。
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/** 该线程任务负责处理服务端发送过来的消息 */
private class ServerHandler implements Runnable{
public void run() {
try{
//通过socket获取输入流读取服务端发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//循环读取服务端发送过来的每一行字符串:
String line;
while((line=br.readLine())!=null){
System.out.println(line);
}
}catch (IOException e){
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
(2)服务端:
package apiday.socket.v8;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
* 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
* (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
* (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*
* 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
* 如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
* java.net.BindException:address already in use
* 绑定异常:地址被使用了
*
* 3、ServerSocket的accept()方法:
* 是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
* 会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
* 阻塞方法:调用后,程序就"卡住"不往下执行了。
*
* 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
*
* 5、定义一个PrintWriter类型数组allOut进行扩容将pw放到数组最后一个元素。实现:遍历allOut数组,将消息发送所有给客户端
*/
public class Server {
/*
java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
(1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
(2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*/
private ServerSocket serverSocket;
private PrintWriter[] allOut = {};
public Server() {
try {
/*
ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
端口建立连接。
如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
java.net.BindException:address already in use
绑定异常:地址被使用了
*/
System.out.println("正在启动服务端");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
try {
/*
ServerSocket的accept方法是一个阻塞方法。
开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
相当于是接电话的动作。
阻塞方法:调用后,程序就"卡住"不往下执行了。
*/
while (true) {
System.out.println("等待客户端连接");
Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
System.out.println("一个客户端连接了!");
//启动一个线程来处理该客户端的交互: Handler:处理器
//①创建线程任务
ClientHandler clientHandler = new ClientHandler(socket);
//②创建1条线程执行该任务
Thread thread = new Thread(clientHandler);
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
private class ClientHandler implements Runnable{
private Socket socket;
//记录远端计算机的地址信息:
private String host;
public ClientHandler(Socket socket){
this.socket = socket;
host = socket.getInetAddress().getHostAddress();
}
public void run() {
try {
//通过socket获取输入流读取对方发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//通过socket获取输出流用于给对方发消息:
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
//将该输出流存入共享数组allOut中:
//(1)扩容:
allOut = Arrays.copyOf(allOut,allOut.length+1);
//(2)将pw放到数组最后一个元素:
allOut[allOut.length-1] = pw;
/*
阻塞:
这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
处于阻塞状态,直到对方发送过来一行字符串为止。
如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
*/
String line;
while ((line = br.readLine())!=null){
System.out.println(host+"说:"+line);
//遍历allOut数组,将消息发送所以给客户端:
for(int i=0;i<allOut.length;i++) {
allOut[i].println(host + "说:" + line);
}
}
} catch (IOException e) {
//e.printStackTrace();
} finally {
}
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
09. v9:服务端完成处理客户端断开连接后的操作
(1)当一个客户端断开连接后,服务端处理该客户端交互的线程ClientHandler应当将通过socket获取
的输出流从共享数组allOut中删除,
(2)防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端
(【前提:catch中调用printStackTrace();方法将异常显示在控制台。】若不处理这个问
题,对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法会抛
出下面异常: java.net.SocketException: connection reset--->服务端无法避免
这个异常。).
Server:
(1)在客户端处理程序:private class ClientHandler implements Runnable{。。。。} 中:
finally{...}中:处理客户端断开连接后的操作:捕获这个异常:socket.close();
(2)在上面ClientHandler中的run():定义:PrintWriter pw = null;
(3)接上面:因为上边初始化了,这里直接赋值就行:把【PrintWriter pw = new PrintWriter(bw, true);】进行赋值
pw = new PrintWriter(bw, true);
(4)在上面【处理客户端断开连接后的操作】中将pw从数组allOut中删除(数组缩容):
(5)在run()方法的下边写一个方法:将消息群发给所有客户端:
(6)将上面的【遍历allOut数组,将消息发送所以给客户端:】 复制粘在上面的方法里————>并将打印语句里换为line
(7)将复制的源代码:【遍历allOut数组将消息发送所以给客户端:】改为:sendMessage(host+"说:"+line); (8)在【将pw放到数组最后一个格子里(元素):】 下面添加:通知所有客户端,该用户上线了:
(9)同理:在run()的try-catch的finally中:通知所有客户端,该用户下线了:
(1)客户端:
package apiday.socket.v9;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 聊天室客户端Client:
* java.net.Socket:————>Socket:插座 套接字
* Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
* 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
*
* 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
*
* 2、
* 01.Socket提供了两个重要的方法:
* (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
* (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
*
* 02.Socket提供了:
* (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
* (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
*
*
* 3、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
*
* 4、创建类ServerHandler 并implements Runnable
*/
public class Client {
/*
java.net.Socket 插座 套接字
Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
读写完成数据交换。
*/
private Socket socket;
/**
* 构造方法:用于初始化客户端
*/
public Client() {
try {
/**
* 实例化Socket时常用的构造方法:Socket(String host,int port)
* 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
* 该机器上的服务端应用程序从而与之建立连接。
*/
//本机IP地址的写法可以是“localhost”
System.out.println("正在连接服务端");
socket = new Socket("192.168.0.104", 8088);
System.out.println("与服务端连接了");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
//启动用于读取服务端发送过来消息的线程:
ServerHandler handler = new ServerHandler();
Thread t = new Thread(handler);
t.start();
/* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
//是一个简易记事本功能。控制台输入的每行字符串都按行写入文件
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
//equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
if ("exit".equalsIgnoreCase(line)) {
break;
}
pw.println(line);//自动行刷新,调用println()方法
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//Socket提供了.close()方法:可以与远端计算机断开连接。
// 该方法调用时,也会自动关闭通过他获取的输入流和输出流。
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/** 该线程任务负责处理服务端发送过来的消息:服务器处理程序 */
private class ServerHandler implements Runnable{
public void run() {
try{
//通过socket获取输入流读取服务端发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//循环读取服务端发送过来的每一行字符串:
String line;
while((line=br.readLine())!=null){
System.out.println(line);
}
}catch (IOException e){
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
(2)服务端:
package apiday.socket.v9;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
* 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
* (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
* (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*
* 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
* 如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
* java.net.BindException:address already in use
* 绑定异常:地址被使用了
*
* 3、ServerSocket的accept()方法:
* 是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
* 会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
* 阻塞方法:调用后,程序就"卡住"不往下执行了。
*
* 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
*
* 5、定义一个PrintWriter类型数组allOut进行扩容将pw放到数组最后一个元素。实现:遍历allOut数组,将消息发送所有给客户端
*
* 6、防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端——————>此时会报异常:
* {对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法会抛出异常:
* java.net.SocketException: connection reset--->服务端无法避免这个异常。}
* (1)解决办法:(处理客户端断开连接后的操作):————>即:将pw从数组allOut中删除(数组缩容):
*/
public class Server {
/*
java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
(1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
(2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*/
private ServerSocket serverSocket;
//存放所有客户端输出流,用于广播消息:
private PrintWriter[] allOut = {};
public Server() {
try {
/*
ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
端口建立连接。
如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
java.net.BindException:address already in use
绑定异常:地址被使用了
*/
System.out.println("正在启动服务端");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
try {
/*
ServerSocket的accept方法是一个阻塞方法。
开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
相当于是接电话的动作。
阻塞方法:调用后,程序就"卡住"不往下执行了。
*/
while (true) {
System.out.println("等待客户端连接");
Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
System.out.println("一个客户端连接了!");
//启动一个线程来处理该客户端的交互: Handler:处理器
//①创建线程任务
ClientHandler clientHandler = new ClientHandler(socket);
//②创建1条线程执行该任务
Thread thread = new Thread(clientHandler);
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
private class ClientHandler implements Runnable{
private Socket socket;
//记录远端客户端计算机的IP信息:
private String host;
public ClientHandler(Socket socket){
this.socket = socket;
host = socket.getInetAddress().getHostAddress();
}
public void run() {
PrintWriter pw = null;
try {
//通过socket获取输入流读取对方发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//通过socket获取输出流用于给对方发消息:
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
pw = new PrintWriter(bw,true);
//将该输出流存入共享数组allOut中:
//(1)扩容:
allOut = Arrays.copyOf(allOut,allOut.length+1);
//(2)将pw放到数组最后一个格子里(元素):
allOut[allOut.length-1] = pw;
//通知所有客户端,该用户上线了:
sendMessage(host + "上线了,当前在线人数:"+allOut.length);
/*
阻塞:
这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
处于阻塞状态,直到对方发送过来一行字符串为止。
如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法
会抛出下面异常: --->服务端无法避免这个异常。
java.net.SocketException: connection reset
*/
String line;
while ((line = br.readLine())!=null){
//遍历allOut数组,将消息发送所以给客户端:
sendMessage(host+"说:"+line);
}
} catch (IOException e) {
//e.printStackTrace();
} finally {
//处理客户端断开连接后的操作:
//将pw从数组allOut中删除(数组缩容):
for(int i=0;i<allOut.length;i++){
if(allOut[i]==pw){//将每一个元素和想删的那个元素比 若是相等就删掉
allOut[i] = allOut[allOut.length-1];
allOut = Arrays.copyOf(allOut,allOut.length-1);
break;
}
}
//通知所有客户端,该用户下线了:
sendMessage(host + "下线了,当前在线人数:"+allOut.length);
try {
socket.close();//与客户端断开链接
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 广播消息给所有客户端(将消息群发给客户端)
* @param line
*/
private void sendMessage(String line){
System.out.println(line);
//遍历allOut数组,将消息发送所以给客户端:
for(int i=0;i<allOut.length;i++) {
allOut[i].println(line);
}
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
10. v10:服务端解决多线程并发安全问题/选取合适的锁对象——>最终版
为了让能叫消息转发给所有客户端,我们在Server上添加了一个数组类型的属性allOut,并且
供所有线程ClientHandler使用,
这时对数组的操作要考虑并发安全问题 :
(1)this不可以
(2)allOut不可以。大多数情况下可以选择临界资源作为锁对象,但是这里不行。 :
从上述代码可以看出,锁定allOut并没有限制多个线程(ClientHandler)操作allOut数组,
还是存在并发安全问题。
(3)可以选取外部类对象作为锁对象,因为这些内部类ClientHandler都从属于这个外部类对象Server.this
还要考虑对数组的不同操作之间的互斥问题,道理同上。因此,对allOut数组的扩容,缩容和
遍历操作要进行互斥。
当两个客户端同时上线(橙,绿):
两个ClientHandler启动后都会对数组扩容,将自身的输出流存入数组
此时ClientHandler(橙)先拿到CPU时间,进行数组扩容:
扩容后发生CPU切换,ClientHandler(绿)拿到时间:
此时ClientHandler(绿)进行数组扩容:
ClientHandler(绿)扩容后,将输出流存入数组最后一个位置:
线程切换回ClientHandler(橙):
ClientHandler(橙)将输出流存入数组最后一个位置,此时会将ClientHandler(绿)存入的输入流覆盖。出现了并发安全问题!!:
选取合适的锁对象:
------this不可以
allOut不可以。大多数情况下可以选择临界资源作为锁对象,但是这里不行。
ClientHandler(橙)锁定allOut:
ClientHandler(橙)扩容allOut
由于数组是定长的,扩容实际是创建新数组,因此扩容后赋值给allOut时,ClientHandler(橙)之前锁定的对象就被GC回收了!而新扩容的数组并没有锁。
若此时发生线程切换,ClientHandler(绿)锁定allOut时,发现该allOut没有锁,因此可以锁定,并执行synchronized内部代码:
ClientHandler(绿)也可以进行数组扩容,那么它之前锁定的数组也被GC回收了!
从上述代码可以看出,锁定allOut并没有限制多个线程(ClientHandler)操作allOut数组,还是存在并发安全问题。
可以选取外部类对象作为锁对象,因为这些内部类ClientHandler都从属于这个外部类对象Server.this
还要考虑对数组的不同操作之间的互斥问题,道理同上。因此,对allOut数组的扩容,缩容和遍历操作要进行互斥。
-------最终代码:
(1)客户端:
package apiday.socket.v10;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 聊天室客户端Client:
* java.net.Socket:————>Socket:插座 套接字
* Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
* 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
*
* 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
*
* 2、
* 01.Socket提供了两个重要的方法:
* (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
* (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
*
* 02.Socket提供了:
* (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
* (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
*
*
* 3、equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
*
* 4、创建类ServerHandler 并implements Runnable
*/
public class Client {
/*
java.net.Socket 插座 套接字
Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP连接,并基于两个流的
读写完成数据交换。
*/
private Socket socket;
/**
* 构造方法:用于初始化客户端
*/
public Client() {
try {
/**
* 实例化Socket时常用的构造方法:Socket(String host,int port)
* 这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到
* 该机器上的服务端应用程序从而与之建立连接。
*/
//本机IP地址的写法可以是“localhost”
System.out.println("正在连接服务端");
socket = new Socket("192.168.0.104", 8088);
System.out.println("与服务端连接了");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start() {
try {
//启动用于读取服务端发送过来消息的线程:
ServerHandler handler = new ServerHandler();
Thread t = new Thread(handler);
t.start();
/* 通过socket获取的字节输出流写出的字节会通过网络发送给远端计算机 */
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
//是一个简易记事本功能。控制台输入的每行字符串都按行写入文件
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
//equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
if ("exit".equalsIgnoreCase(line)) {
break;
}
pw.println(line);//自动行刷新,调用println()方法
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//Socket提供了.close()方法:可以与远端计算机断开连接。
// 该方法调用时,也会自动关闭通过他获取的输入流和输出流。
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/** 该线程任务负责处理服务端发送过来的消息:服务器处理程序 */
private class ServerHandler implements Runnable{
public void run() {
try{
//通过socket获取输入流读取服务端发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//循环读取服务端发送过来的每一行字符串:
String line;
while((line=br.readLine())!=null){
System.out.println(line);
}
}catch (IOException e){
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
(2)服务端:将数组换为集合:
package apiday.socket.v10;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
/**
* 聊天室服务端Server:——————>如果我们将Socket比喻为"电话",那么ServerSocket相当于"总机"
* 1、java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
* (1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
* (2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*
* 2、ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个端口建立连接。
* 如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
* java.net.BindException:address already in use
* 绑定异常:地址被使用了
*
* 3、ServerSocket的accept()方法:
* 是一个阻塞方法;开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
* 会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。相当于是接电话的动作。
* 阻塞方法:调用后,程序就"卡住"不往下执行了。
*
* 4、创建线程:实现Runnable的方式————>使用多线程实现多客户端连接服务端
*
* 5、定义一个PrintWriter类型数组allOut进行扩容将pw放到数组最后一个元素。实现:遍历allOut数组,将消息发送所有给客户端
*
* 6、防止其他的ClientHandler再将消息通过这个输出流发送给当前客户端——————>此时会报异常:
* {对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法会抛出异常:
* java.net.SocketException: connection reset--->服务端无法避免这个异常。}
* (1)解决办法:(处理客户端断开连接后的操作):————>即:将pw从数组allOut中删除(数组缩容):
*
* 7、选取合适的锁对象:-------外部类名.this(内部类访问外部类对象)
* (1)写一个同步块————>synchronized (Server.this) { 扩容、放在最后一个元素}
* (2)写一个同步块————>synchronized (Server.this) { for(){if(){缩容、放在最后一个元素}}}
* (3)将【广播消息给所有客户端】中的也放在同步块里:synchronized (Server.this) {打印语句、for}
*/
public class Server {
/*
java.net.ServerSocket:ServerSocket是运行在服务端上的。其主要有两个作用:
(1)向服务端申请服务端口(客户端Socket就是通过这个端口与服务端建立连接的)
(2)监听服务端口,一旦客户端连接会立即创建一个Socket,通过该Socket与客户端交互
*/
private ServerSocket serverSocket;
//存放所有客户端输出流,用于广播消息:
// private PrintWriter[] allOut = {};
/* 将数组换成集合的形式:*/
private Collection<PrintWriter> allOut = new ArrayDeque<>();
public Server() {
try {
/*
ServerSocket在创建的时候要申请一个固定的端口号,客户端才能通过这个
端口建立连接。
如果该端口被当前操作系统中其他程序使用了,那么这里实例化会抛出异常:
java.net.BindException:address already in use
绑定异常:地址被使用了
*/
System.out.println("正在启动服务端");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
try {
/*
ServerSocket的accept方法是一个阻塞方法。
开始等待客户端的连接,一旦一个客户端通过端口建立连接,此时accept方法
会立即返回一个Socket实例。通过该实例可以与该客户端进行交互。
相当于是接电话的动作。
阻塞方法:调用后,程序就"卡住"不往下执行了。
*/
while (true) {
System.out.println("等待客户端连接");
Socket socket = serverSocket.accept();//accept方法是一个阻塞方法:调用后,程序就"卡住"不往下执行了。
System.out.println("一个客户端连接了!");
//启动一个线程来处理该客户端的交互: Handler:处理器
//①创建线程任务
ClientHandler clientHandler = new ClientHandler(socket);
//②创建1条线程执行该任务
Thread thread = new Thread(clientHandler);
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/** 该线程任务是用一个线程来处理一个客户端的交互——————>客户端处理程序 */
private class ClientHandler implements Runnable{
private Socket socket;
//记录远端客户端计算机的IP信息:
private String host;
public ClientHandler(Socket socket){
this.socket = socket;
host = socket.getInetAddress().getHostAddress();
}
public void run() {
PrintWriter pw = null;
try {
//通过socket获取输入流读取对方发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//通过socket获取输出流用于给对方发消息:
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out,StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
pw = new PrintWriter(bw,true);
//将该输出流存入共享数组allOut中(存放着所有客户输出流):(在下面实现将消息回复给所有客户端)
/*
①不可以:
因为this是ClientHandler。不同线程是不同的ClientHandler任务
synchronized (this) {
②不可以:
通常情况下,同步监视器对象选取的就是多个线程并发操作的临界资源。这里临界资源
就是数组allOut。但是这里之所以不可以是因为同步块中有对该数组扩容的操作,这
会导致allOut对象发生了变化,其他需要同步执行该代码块看到的就不再是之前的allOut
导致同步失效
synchronized (allOut) {
*/
synchronized (Server.this) {
//(1)扩容:
//allOut = Arrays.copyOf(allOut, allOut.length + 1);
//(2)将pw放到数组最后一个格子里(元素):
//allOut[allOut.length - 1] = pw;
allOut.add(pw);
}
//通知所有客户端,该用户上线了:
//sendMessage(host + "上线了,当前在线人数:"+allOut.length);
sendMessage(host + "上线了,当前在线人数:"+allOut.size());
/*
阻塞:
这里的BufferedReader读取时底下连接的流是通过Socket获取的输入流,
当远端计算机还处于连接状态,但是暂时没有发送内容时,readLine方法会
处于阻塞状态,直到对方发送过来一行字符串为止。
如果返回值为null,则表示对方断开了连接(对方调用了socket.close())。
对于windows的客户端而言,如果是强行杀死的进程,服务端这里readLine方法
会抛出下面异常: --->服务端无法避免这个异常。
java.net.SocketException: connection reset
*/
String line;
while ((line = br.readLine())!=null){
//遍历allOut数组,将消息发送所以给客户端:
sendMessage(host+"说:"+line);
}
} catch (IOException e) {
//e.printStackTrace();
} finally {
//处理客户端断开连接后的操作:
synchronized (Server.this) {
//将pw从数组allOut中删除(数组缩容):
// for (int i = 0; i < allOut.length; i++) {
// if (allOut[i] == pw) {//将每一个元素和想删的那个元素比 若是相等就删掉
// allOut[i] = allOut[allOut.length - 1];
// allOut = Arrays.copyOf(allOut, allOut.length - 1);
// break;
// }
// }
allOut.remove(pw);
}
//通知所有客户端,该用户下线了:
// sendMessage(host + "下线了,当前在线人数:"+allOut.length);
sendMessage(host + "下线了,当前在线人数:"+allOut.size());
try {
socket.close();//与客户端断开链接
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 广播消息给所有客户端(将消息群发给客户端)
* @param line
*/
private void sendMessage(String line){
synchronized (Server.this) {
System.out.println(line);
//遍历allOut数组,将消息发送所以给客户端:
// for (int i = 0; i < allOut.length; i++) {
// allOut[i].println(line);
// }
for(PrintWriter pw : allOut){
pw.println(line);
}
}
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
11. v11:非阻塞IO(NIO)实现聊天室服务端
(1)客户端:
package apiday.socket.v11;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 聊天室客户端Client:
* java.net.Socket:————>Socket:插座 套接字
* Socket封装了TCP协议的通讯细节,使用它可以和服务端建立TCP网络链接,
* 并通过它获取两个流(一个输入一个输出),并基于两个流的读写完成与服务端的数据交互。
*
* 1、实例化Socket时常用的构造方法:Socket(String host,int port):这个构造器实例化Socket的过程就是与服务端建立连接的过程。
* 参数1:服务端的IP地址
* 参数2:服务端开启的服务端口
* 我们通过服务端的IP可以找到网络上服务端所在的计算机。通过端口号可以找到该机器上的服务端应用程序从而与之建立连接。
*
* 2、
* 01.Socket提供了两个重要的方法:
* (1)OutputStream getOutputStream():该方法会获取一个字节输出流,通过这个输出流写出的字节数据会通过网络发送给对方。
* (2)InputStream getInputStream():通过该方法获取的字节输入流读取的是远端计算机发送过来的数据。
*
* 02.Socket提供了:
* (1).close()方法:可以与远端计算机断开连接。该方法调用时,也会自动关闭通过他获取的输入流和输出流。
* (2).getInetAddress().getHostAddress:获取远端客户端的地址信息:
*
*
* 3、String提供方法:equalsIgnoreCase() 方法用于将字符串与指定的对象比较,不考虑大小写。
*
* 4、创建类ServerHandler 并implements Runnable
*/
public class Client {
/*
java.net.Socket 套接字
封装了TCP协议的通讯细节,使用它可以和远端计算机建立TCP链接,并基于两条流的读写操作
完成与远端计算机的数据交换。
*/
private Socket socket;
/**
* 构造方法,用于初始化客户端
*/
public Client(){
try {
/*
实例化Socket时通常需要传入两个参数:
1:远端计算机的地址信息(IP) 192.168.1.1
2:远端计算机开启的服务端口
我们通过地址信息找到网络上的服务端计算机,通过端口链接到该机器上运行的服务端
应用程序。
实例化的过程就是与服务端链接的过程,若链接失败会抛出异常!
*/
System.out.println("正在连接服务端...");
socket = new Socket("localhost",8088);
System.out.println("与服务端建立连接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端开始工作的方法
*/
public void start(){
//启动一条线程来处理读取服务端发送过来消息的操作
ServerHandler handler = new ServerHandler();
Thread t = new Thread(handler);
t.setDaemon(true);//如果不再发消息了也就不用再读客户端消息了,所以可以设置为守护线程
t.start();
try {
/*
通过Socket的方法getOutputStream()可以获取一个字节输出流
使用这个输出流写出的字节会发送给远端计算机.
*/
OutputStream out = socket.getOutputStream();
//转换输出流(高级流),1:负责衔接字符与字节流 2:将写出的字符转换为字节
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
//缓冲字符输出流(高级流),负责块写文本数据提高写出效率
BufferedWriter bw = new BufferedWriter(osw);
//PrintWriter(高级流),1:按行写出字符串 2:自动行刷新
PrintWriter pw = new PrintWriter(bw,true);
Scanner scanner = new Scanner(System.in);
System.out.println("请开始输入内容,单独输入exit退出。");
while(true) {
String line = scanner.nextLine();
if("exit".equals(line)){
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally{
//处理与服务端断开链接的操作
try {
//调用socket的close可以与远端计算机断开链接,并且自动关闭通过它获取的流
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
/**
* 该线程任务负责读取服务端发送过来的消息
*/
private class ServerHandler implements Runnable{
public void run(){
try {
//通过socket获取输入流,用于读取服务端发送过来的消息
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String line;
//循环读取服务端发送过来的每一条消息并输出
while((line = br.readLine())!=null){
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
(2)NIO实现聊天室服务端
package apiday.socket.v11;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* NIO实现聊天室服务端
*
* netty框架:对nio的封装
*/
public class NIOServer {
public static void main(String[] args) {
try {
//存放所有客户端的SocketChannel,用于广播消息
List<SocketChannel> allChannel = new ArrayList<>();
//创建总机,ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//ServerSocketChannel模式为阻塞模式,可以将其设置为非阻塞模式
serverSocketChannel.configureBlocking(false);//传入false设置为非阻塞模式
//为ServerSocketChannel绑定服务端口,客户端可以通过该端口与我们建立连接
serverSocketChannel.bind(new InetSocketAddress(8088));
//以上创建为固定模式,以后都可以用这样的形式创建ServerSocketChannel的非阻塞模式
/*
多路选择器的应用
这个是NIO解决非阻塞的关键API,用于监听所有通道对应的事件,并做出对应的操作。
我们的线程只要轮询处理多路选择器中待处理的通道事件即可完成所有通道的工作,避免使用大量线程
处于阻塞来减少不必要的系统开销。
*/
Selector selector = Selector.open();//使用其静态方法open获取一个多路选择器实例
/**
* 让"总机"ServerSocketChannel向多路选择器上注册一个事件,即:accept事件。
* 原因:原来使用ServerSocket时,一旦主线程调用accept方法就会进入阻塞状态,直到一个客户端连接
* 否则将无法继续执行其他工作。而现在的操作是让多路选择器关心该事件,避免让线程处于阻塞。
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
/*
多路选择器的select方法
当注册在该选择器上的channel有事件待处理时,此方法会立即返回一个int值,表示有多少个事件待处理
若没有任何事件待处理时,此方法会形成阻塞。
*/
System.out.println("等待选择器告知是否有事件等待处理...");
selector.select();
System.out.println("选择器有事件待处理!!!");
//通过选择器获取所有待处理的事件
Set<SelectionKey> keySet = selector.selectedKeys();
//遍历集合,将所有待处理的事件处理完毕
for (SelectionKey key : keySet) {
//判断该事件是否为可以接受一个客户端连接了(对应的是向多路选择器注册的事件SelectionKey.OP_ACCEPT)
if (key.isAcceptable()) {
//处理接收客户端连接的操作
/*
通过SelectionKey的方法channel()获取该事件触发的通道
因为只有ServerSocketChannel向多路选择器注册了OP_ACCEPT事件,因此当该事件
isAcceptable()返回值为true,则说明该事件一定是由ServerSocketChannel触发的
所以我们通过该事件获取触发该事件的通道时,一定获取的是ServerSocketChannel
*/
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
/*
获取的SocketChannel与原来ServerSocket.accept返回的Socket道理一致
通过该SocketChannel就可以与连接的客户端进行双向交互数据了
*/
SocketChannel socket = channel.accept();//接受客户端的连接
/*
非阻塞的ServerSocketChannel就算多路选择器提示有客户端请求可接受了,accept返回时
得到的SocketChanel有可能为null
*/
if (socket == null) {
continue;//忽略本次事件的后续处理
}
/*
当我们接受了客户端连接后,原来的聊天室服务端我们通过Socket获取输入流读取客户端
发送过来消息的操作时如果客户端不发送内容,那么读取操作就会阻塞!
对此,我们将当前SocketChannel它的读取消息操作也注册到多路选择器中,这样一来只有
当客户端发送过来消息时,多路选择器才会通知我们进行处理。
*/
//将当前SocketChannel设置为非阻塞模式
socket.configureBlocking(false);
//向多路选择器中注册读取客户端消息的事件
socket.register(selector, SelectionKey.OP_READ);
//将该SocketChannel存入共享集合,用于将消息广播
allChannel.add(socket);
System.out.println("一个客户端连接了!当前在线人数:"+allChannel.size() );
//该事件是否为某个SocketChannel有消息可以读取了(某个客户端发送过来了消息)
}else if(key.isReadable()){
try {
//通过事件获取触发了该事件的channel
SocketChannel socketChannel = (SocketChannel) key.channel();
//通过SocketChannel读取该客户端发送过来的消息
ByteBuffer buffer = ByteBuffer.allocate(1024);
//非阻塞状态下,有可能读取数据时未读取到任何字节
int len = socketChannel.read(buffer);
if (len == 0) {
continue;
} else if (len == -1) {//若本次读取的长度返回值为-1则表示客户端断开连接了
socketChannel.close();//关闭SocketChannel与客户端也断开
allChannel.remove(socketChannel);
continue;
}
buffer.flip();//反转后position到limit之间就是本次读取到的数据了
byte[] data = new byte[buffer.limit()];
/*
Buffer的get方法要求我们传入一个字节数组,作用是将当前Buffer中从下标
position开始的连续data数组长度的字节量装入该data数组。
*/
buffer.get(data);//调用完毕后,data中保存的就是buffer中本次读取到的所有字节了
//将读取的消息转换为字符串(客户端发送过来的消息)
String line = new String(data, StandardCharsets.UTF_8);
//通过SocketChannel获取客户端地址信息
String host = socketChannel.socket().getInetAddress().getHostAddress();
System.out.print(host+"说:" + line);
//遍历所有的SocketChannel,将该消息发送给所有客户端
for(SocketChannel sc : allChannel){
buffer.flip();//position:0 limit:buffer中所有之前读取到的字节
sc.write(buffer);//position=limit=buffer中所有之前读取到的字节
}
}catch(IOException e){
//读取客户端消息这里若抛出异常,则通常是客户端强行断开连接造成的。
key.channel().close();//断开该SocketChannel与客户端断开连接即可
allChannel.remove(key.channel());
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}