2022Java学习笔记七十九(网络编程:TCP通信,TCP通信:多个客户端消息【重点】,追踪客户端的上线和下线功能、线程池优化)
一、TCP通信快速入门
TCP协议回顾:
1、TCP是一种面向连接,安全、可靠的传输数据的协议
2、传输前,采用“三次握手”方式,点对点通信,是可靠的
3、在连接中可进行大数据量的传输
构造器和常用API
二、TCP客户端发送消息
示例代码
package com.zcl.d12_tcpDaemo;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
/*
// 目标:完成socket网络编程入门案例的客户端开发 一发一收功能
*/
public class ColientDemo {
public static void main(String[] args) {
System.out.println("-------- 客户端启动成功 --------");
try {
// 1、创建socket通信管道请求服务器的连接1
/**
* 参数一:服务端的IP地址
* 参数二:服务端的端口
*/
Socket socket = new Socket("127.0.0.1",7777);
// 2、从socket通道中得到一个字节输出流,负责发送数据
OutputStream ops = socket.getOutputStream();
// 把低级的字节流包装成打印流
PrintStream ps = new PrintStream(ops);
// 发送消息
ps.println("我对您发起邀请"); // 必须是发一行的消息
ps.flush();
// 不要关闭打印流,需要用户发送关闭消息才关闭
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、服务端代码编写
示例代码
package com.zcl.d12_tcpDaemo;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/*
目标:开发socket网络编程入门代码的服务端,实现接收消息
*/
public class ServerDemo {
public static void main(String[] args) {
System.out.println("-------- 服务端启动成功 --------");
try {
// 1、注册服务端
ServerSocket serverSocket = new ServerSocket(7777);
// 2、必须调用accept方法,等待接收客户端的socket连接请求,建立socket通信管道
Socket socket = serverSocket.accept();
// 3、从socket通信管道中得到一个字节输入
InputStream is = socket.getInputStream();
// 4、把字符输入流包转成缓存字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照缓存字节输入流按行读取消息
String msg;
if ((msg = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress()+"说了:"+msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
需要严格遵守一发一收的原则,如果客户端不是使用一行消息来发送的化,而服务端使用的是接收一行数据就会报错,因为客户端不是发送一行服务端就接收到不完整的消息
如果客户端只发送一条行数据,而服务端使用循环while()接收数据也会报错,因为客户端已经完成了一次消息发送已经关掉了,服务器还在接收数据就会报错
三、TCP通信:多发多收案例
在上面的代码基础上分别个客户端和服务端添加反复循环就可以了
客户端代码修改
while (true) {
System.out.println("请输入需要发送的消息:");
String msg = sc.nextLine();
// 发送消息
ps.println(msg); // 必须是发一行的消息
ps.flush(); // 刷新
}
服务端代码修改
while ((msg = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress()+"说了:"+msg);
}
现在写好的服务端目前自能同时一收一个客户端的消息,原因是:目前的服务端是单线程的,每次只能处理一个客户端的消息
四、实现同时接收多个客户端消息【重点】
引用多线程
实现代码
1、客户端的代码不要动
2、修改服务端的代码
package com.zcl.d13_socket4;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/*
目标:开发socket网络编程入门代码的服务端,实现服务端可以同时接收多个客户端消息
*/
public class ServerDemo {
public static void main(String[] args) {
System.out.println("-------- 服务端启动成功 --------");
try {
// 1、注册服务端
ServerSocket serverSocket = new ServerSocket(7777);
while (true) {
// 2、每接收到一个客户端的socket管道,交给一个独立的子线程负责读取消息
Socket socket = serverSocket.accept();
// 3、开始创建独立线程处理socket
new ServerReaderThread(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3、创建一个多线程的ServerReaderThread类实现Thread类
package com.zcl.d13_socket4;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
// 3、从socket通信管道中得到一个字节输入
InputStream is = socket.getInputStream();
// 4、把字符输入流包转成缓存字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照缓存字节输入流按行读取消息
String msg;
while ((msg = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress()+"说了:"+msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
多客户端接收消息就可以完成了
关于如何追踪客户端的上线和下线功能
1、在服务端的while循环里面通过socket的IP地址可以知道哪台客户端上线了
// 判断可客户端谁上线了 System.out.println(socket.getRemoteSocketAddress()+"他上线了");
2、在定义的ServerReaderThread多线程类里面,将捕获catch中最终IP地址判断下线,如果客户端报错了服务端就会给那个客户端报错
// 用户下线通知
System.out.println(socket.getRemoteSocketAddress() + "他下线了");
在定义的ServerReaderThread多线程类
五、TCP通信:线程池优化
1、客户端代码不需要动
2、修改服务端代码
package com.zcl.d14_scoket5;
import com.zcl.d13_socket4.ServerReaderThread;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
/*
目标:开发socket网络编程入门代码的服务端,实现服务端可以同时接收多个客户端消息
*/
public class ServerDemo {
// 使用静态变量记住一个线程池对象
private static ExecutorService pool = new ThreadPoolExecutor(3,5,6,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(2),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
System.out.println("-------- 服务端启动成功 --------");
try {
// 1、注册服务端
ServerSocket serverSocket = new ServerSocket(6666);
while (true) {
// 2、每接收到一个客户端的socket管道,交给一个独立的子线程负责读取消息
Socket socket = serverSocket.accept();
// 判断可客户端谁上线了
System.out.println(socket.getRemoteSocketAddress()+"他上线了");
// 任务对象负责读取消息
Runnable target = new ServerReaderRunnable(socket); // 提交线程池排队
pool.execute(target);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
添加了ExecutorService 线程池对象,通过线程池提交客户端发起的信息,在线程池排队
3、创建ServerReaderRunnable任务对象实现Runnable接口
需要重写构造器接收客户端发送对象
package com.zcl.d14_scoket5;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
public class ServerReaderRunnable implements Runnable{
// 接收对象
private Socket socket;
// 有参构造器接收对象
public ServerReaderRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 3、从socket通信管道中得到一个字节输入
InputStream is = socket.getInputStream();
// 4、把字符输入流包转成缓存字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照缓存字节输入流按行读取消息
String msg;
while ((msg = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress()+"说了:"+msg);
}
} catch (Exception e) {
// e.printStackTrace();
// 用户下线通知
System.out.println(socket.getRemoteSocketAddress() + "他下线了");
}
}
}
使用线程池的优势在哪里
1、服务器端可以复用线程池处理多个客户端,可以避免系统瘫痪
2、适合客户端通信时长较短的场景