一、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、适合客户端通信时长较短的场景