服务端:
package socket;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 聊天室服务端
*/
public class Server {
/*
java.net.ServerSocket
运行在服务端的ServerSocket主要工作:
1:向系统申请对外的服务端口,客户端就是通过这个端口与服务端建立连接的
2:监听服务端口,一旦一个客户端建立连接立即接受并获取一个Socket实例与之交互
将ServerSocket想象为某客服呼叫中心的"总机"。用户拨打电话总是打给总机,然后
总机下面连接着若干部电话,分配一台电话与该用户沟通,从而做到服务端可以同时与
多个用户沟通的效果。
*/
private ServerSocket serverSocket;//主机-----服务器 接口
//存放对应所有客户端的输出流,用于广播消息
private List<PrintWriter> allOut = new ArrayList<>();
public Server() {
try {
System.out.println("正在启动服务端");
/**
* ServerSocket在创建是需要指定对外的服务端口,
* 该端口不能与服务器上其他运行的应用程序申请的端口一致,
* 否则会报错:
* java.net.BindException:address already in use
* 告知该地址已经被占用了。
* 此时需要更换端口,或者自行在操作系统中杀死占用该端口的应用程序。
*/
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()
* 通过Socket获取的输入流可以读取远端计算机发送过来的数据
* /
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in , StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
//读取来自客户端发送过来的一行字符串
String line ;
/*
* 当客户端异常断开连接(如果客户端没有调用socket。close()断开)
*
* windows的客户端如果异常断开:服务端这里通常readLine方法会直接抛出
* 异常:java.net.SocketException:Connection reset
*
* linus的客户端异常断开: 服务端这里readLine通常返回null。
* /
while ((line = br.readLine())!=null) {
//如果不写null 在苹果或者linus系统中是不会出现异常自动关闭的,会一直读下去,所以这里的null是出于全平台考虑
System.out.println("客户端说:" + line);
}
*/
/** 启动一个线程来处理与该客户端的交互 */
ClientHandler handler = new ClientHandler(socket);
Thread t = new Thread(handler);
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//将线程定义成一个内部类
/**
* 该线程任务负责与指定的客户端进项交互
*/
private class ClientHandler implements Runnable {
/**
* 记录客户端的IP地址
*/
private String host;
/**
* 参数的传递
*/
private Socket socket; //属性
public ClientHandler(Socket socket) { //构造器
this.socket = socket; //初始化。赋值给属性
//通过socket获取远端计算机(客户端)的IP地址信息
host = socket.getInetAddress().getHostAddress();
}
/**
* 传递参数思想:在类中定义了某个成员方法,这个成员方法需要一个外部的变量,
* 但是这个值我们没有办法,通过定义这个方法传参给我们的时候,我们就可以利用这总思路
* ------------------------------------------------------------------
* 当我们没有办法通过一个方法直接把值传给你的时候,我们可以了利用这样的方式将参数传递
* 构造器一定是于成员方法之前执行,
* 而属性又能被构造器访问,又能被成员方法访问
* 基于这两个特点,
* 我们可以先定义一个属性,
* 在构造器当中将外面的值先赋予给属性,
* 然后再在成员方法中使用,
* 就能使用到外面的那个值了
*/
public void run() { //成员方法
PrintWriter pw = null;//扩大pw的作用域, 注意:局部变量使用之前必须初始化,以防IO异常
try {
/*
* Socket重要的方法:
* InputStream getInputStream()
* 通过Socket获取的输入流可以读取远端计算机发送过来的数据
*/
InputStream in = socket.getInputStream(); //socket这个外部值 就能使用了
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);
//将该输出流存入到贡献的集合中,以便其他ClientHandler可以将消息发送给这个客户端
synchronized (allOut) {//锁临界资源最合适(抢谁锁谁)//上锁是为了时间片不够,乱删
allOut.add(pw);
}
//广播该客户端上线的信息
sendMessage(host+"上线了,当前在线人数:"+allOut.size());
//读取来自客户端发送过来的一行字符串
String line;
/*
* 当客户端异常断开连接(如果客户端没有调用socket。close()断开)
*
* windows的客户端如果异常断开:服务端这里通常readLine方法会直接抛出
* 异常:java.net.SocketException:Connection reset
*
* linus的客户端异常断开: 服务端这里readLine通常返回null。
*/
while ((line = br.readLine()) != null) {
/*
//如果不写null 在苹果或者linus系统中是不会出现异常自动关闭的,会一直读下去,所以这里的null是出于全平台考虑
System.out.println(host + "说:" + line);
//将消息发送给客户端
//pw.println(host+"说:" +line);
//将消息发送给 所有 客户端
for (PrintWriter p : allOut){
p.println(host+"说:"+line);
}
*/
sendMessage(host+"说:"+line);
}
} catch (IOException e) {
/* e.printStackTrace();
//如果客户端强行退出,此处会报错,红色一堆,
//如若不想看到,可将e.printStackTrace();删除
//以达到他退出,我们看不到报错信息,但是会迷茫
//所以要用finally,去解决我们迷茫的问题
*/
} finally {
//统一处理该客户端下线后的工作
//1)将该客户的输出流从allOut中删除
synchronized (allOut) {//上锁是为了时间片不够,乱删
allOut.remove(pw);
}
sendMessage(host + "下线了,当前在线人数:" + allOut.size());
//2)将socket关闭释放资源
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 将给定的消息发送给所有客户端
* @param line
*/
private void sendMessage(String line){
synchronized (allOut) {//上锁是为要和增删操作做互斥的,就是为了让他在读取写出时,不能增删,否则乱套
System.out.println(line);
//将消息发送给所有客户端
for (PrintWriter p : allOut) {//新循环遍历就是迭代器遍历,迭代器在遍历时,时不能够增删元素的,否则会报错的
p.println(line);
}
}
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
}
客户端:
package socket;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 聊天室客户端
*/
public class Client {
/**
* java.net.Socket 套接字
* Socket封装了TCP协议的通讯细节,
* 使得我们使用它就可以与远端计算机建立连接并基于两条流(一条输入一条输出)的读写完成与远端计算机的数据交换。
*
* 将Socket比喻成“手机”,我们需要先与对方拨通电话建立连接,
* 然后通过通听筒(输入流)和麦克风(输出流)与对方变流。
*/
private Socket socket;
/**
* 构造方法用来初始化客户端,主要工作是与服务端建立TCP连接
*/
public Client() {
try {
/**
* Socket常用的构造器
* Socket(String host , int port)
* 参数1:要连接的服务端的IP地址,用于找到服务端所在的计算机(服务器)
* 参数2:服务端打开的服务端口,通过该端口连接到连接到服务器上运行的服务端应用程序
*
* 实例化的过程就是连接服务端的过程,如果连接失败会抛出异常
*/
System.out.println("正在连接服务端。。。");
socket = new Socket("localhost",8088);
//localhost 是 本地服务器地址 此处替换IP地址,可连接其他IP地址
System.out.println("与服务端成功建立连接!");
}catch (IOException e ){
e.printStackTrace();
}
}
//客户端开始工作的方法
public void start() {
//将一个字符串发送给服务端
try {
//启动用于读取服务端发送过来消息的线程
ServerHandler handler = new ServerHandler();
Thread t = new Thread(handler);
t.setDaemon(true);//设置为守护线程,该线程不聊天退出,此读取消息的线程也跟着结束
t.start();
/*
* Socket重要的方法:
* OutputStream getOutputStream()
* 此方法会返回一个字节输出流,
* 通过这个字节输出流写出的字节会通过网络发送给远端计算机
*/
//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();
if ("exit".equals(line)) {
break;
}
pw.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
/*
* socket 的 close 方法
* 1: 调用时会自动关闭通过socket获取的输入流与输出流
* 2: 与对方进行4次挥手断开操作
*/
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 {
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();
}
}
}
}