TCP通信
一、TCP协议回顾
- TCP是一种
面向连接,安全、可靠
的传输数据的协议。 - 传输前,采用“三次握手”方式,
点对点通信
,是可靠的。 - 在连接中可进行大数据量的传输。
二、TCP通信模式演示
- TCP是点到点通信,因此需要建立一个点到点通信的管道来进行数据传输。
- 以下是客户端传输数据给服务端:
- 以下是服务端传输数据给客户端:
注意:在Java中只要是使用java.net.Socket类实现通信,底层就是使用了TCP协议。
三、Socket类
1、构造器
构造器 | 说明 |
public Socket(String host, int port) | 创建发送端的Socket对象与服务端连接,参数为服务端程序的IP和端口。 |
2、API
方法名称 | 说明 |
OutputStream getOutputStream() | 得到字节输出流对象 |
InputStream getInputStream() | 得到字节输入流对象 |
四、ServerSocket类
1、构造器
构造器 | 说明 |
public ServerSocket(int port) | 注册服务端端口 |
2、API
方法名称 | 说明 |
public Socket accept() | 等待接收客户端的Socket通信连接 连接成功返回Socket对象与客户端建立端到端通信(点到点通信) |
五、TCP通信入门案例:一发一收
1、客户端发送消息
(1)实现步骤
- 创建客户端的Socket对象,请求与服务端的连接。
- 使用Socket对象调用getOutputStream()方法得到字节输出流。
- 使用字节输出流完成数据的发送。
- 释放资源:关闭socket管道。
package com.app.d6_tcp_socket1;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
/**
客户端(发送端)
需求:创建客户端与服务端建立连接,完成一发一收
*/
public class ClientDemo1 {
public static void main(String[] args) {
System.out.println("-------------客户端启动--------------");
try {
/**
1、创建Socket对象,请求与服务端连接
构造器:public Socket(String host, int port)
参数一:服务端的IP地址
参数二:服务端的端口
*/
Socket socket = new Socket("127.0.0.1", 6666);
// 2、获得字节输出流对象
OutputStream os = socket.getOutputStream();
// 3、将低级的字节输出流包装成高级的打印流,负责发送数据
PrintStream ps = new PrintStream(os);
// 4、发送数据
ps.print("我是TCP通信客户端,已与你建立了连接,现发出邀请:约吗?");
ps.flush(); // 刷新数据
/**
释放资源:
不建议在这里使用,因为客户端已经与服务端建立了通信管道,
在这里释放资源,容易出现丢失数据的情况,
因为必须是客户端确认离线了才释放资源、关闭管道。
*/
// socket.close();
} catch (Exception e) { // 捕获异常
e.printStackTrace();
}
}
}
(2)小结
1、TCP通信的客户端的代表类是?
- Socket类
- public Socket(String host, int port)
2、TCP通信如何使用Socket管道发送、接收数据?
- getOutputStream()方法:获得字节输出流对象(发)
- getInputStream()方法:获得字节输入流对象(收)
2、服务端接收消息
(1)实现步骤
- 创建ServerSocket对象并注册端口。
- 使用ServerSocket对象的accept方法,等待客户端的Socket连接请求,建立Socket通信管道。
- 从socket通信管道中得到字节输入流,负责读取接收数据。
- 将低级的字节输入流转换成字符输入流。
- 将字符输入流包装成高级的缓冲字符输入流。
- 使用while循环按照行读取消息
package com.app.d6_tcp_socket1;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
*/
public class ServerDemo2 {
public static void main(String[] args) {
System.out.println("-------------服务端启动--------------");
try {
// 1、创建ServerSocket对象并注册端口
ServerSocket serverSocket = new ServerSocket(6666);
// 2、使用ServerSocket对象的accept方法,等待客户端的Socket请求,建立Socket通信管道
Socket socket = serverSocket.accept();
// 3、从socket通信管道中得到字节输入流
InputStream is = socket.getInputStream();
// 4、将低级的字节输入流转换成字符输入流
// InputStreamReader isr = new InputStreamReader(is);
// 5、将低级的字符输入流包装成高级的缓冲字符输入流:负责读取接收数据
// BufferedReader br = new BufferedReader(isr);
BufferedReader br = new BufferedReader(new InputStreamReader(is)); // 一步到位,省一行代码
// 6、使用while循环按照行读取数据
String msg; // 定义消息变量,用于存储按照行读取接收到的消息数据
while ((msg = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
}
} catch (Exception e) { // 捕获异常
e.printStackTrace();
}
}
}
(2)小结
1、TCP通信服务端用的代表类是?
- ServerSocket类,注册端口。
- 调用accept()方法阻塞等待接收客户端的连接请求,得到Socket对象。
2、TCP通信的基本原理?
- 客户端怎么发,服务端就应该怎么收。
- 客户端如果没有消息,服务端会进入阻塞等待。
- Socket一方关闭或者出现异常,对方Socket也会失效或者出错。
3、测试
六、TCP通信案例:多发多收
1、需求
- 使用TCP通信方式实现:多发多收消息。
2、分析
- 客户端:
- 1、创建Socket对象,指定服务端IP地址和端口,请求与服务端连接。
- 2、调用Socket对象的getOutputStream()方法,得到字节输出流。
- 3、将低级的字节输出流包装成高级的打印流。
- 4、创建键盘输入对象,用于客户端输入消息。
- 5、使用while死循环不断让客户端输入消息,如果输入的是exit则说明客户端已离线!!释放资源;
- 6、如果客户端输入的消息不是exit,则将客户端输入的消息发送给服务端。
package com.app.d7_tcp_socket2;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
需求:开发客户端多发消息给服务端,输入exit表示离线、终止程序!
*/
public class ClientDemo1 {
public static void main(String[] args) {
System.out.println("-=-=-=-=-客户端启动-=-=-=-=-");
try {
// 1、创建Socket对象,指定服务端的IP地址和端口,请求与服务端连接
Socket socket = new Socket("127.0.0.1", 6565);
// 2、调用Socket对象的getOutputStream()方法,得到字节输出流。
OutputStream os = socket.getOutputStream();
// 3、将低级的字节输出流包装成高级的打印流
PrintStream ps = new PrintStream(os);
// 4、创建键盘输入对象,用于客户端输入消息
Scanner sc = new Scanner(System.in);
// 5、使用while死循环不断让客户端输入消息,
while (true) {
// 让客户端输入消息
System.out.println("请说:");
String msg = sc.nextLine();
// 如果输入的是exit说明客户端已离线!!释放资源。
if (msg.equals("exit")) {
System.out.println("您已离线!!");
socket.close(); // 释放资源!
return;
}
// 6、如果客户端输入的消息不是exit,程序将会走到这里,则将客户端输入的消息发送给客户端。
ps.println(msg);
ps.flush(); // 必须刷新数据
}
} catch (Exception e) { // 捕获异常!
e.printStackTrace();
}
}
}
- 服务端:
- 1、创建ServerSocket对象,注册端口。
- 2、调用ServerSocket对象的accept方法,等待客户端的Socket请求,建立Socket通信管道。
- 3、从Socket通信管道中得到字节输入流。
- 4、将低级的字节输入流转换成字符输入流,并包装成高级的缓冲字符输入流。
- 5、使用while死循环不断等待客户端发送消息,接收后展示。
package com.app.d7_tcp_socket2;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
需求:开发服务端多收客户端发送过来的消息!
*/
public class ServerDemo2 {
public static void main(String[] args) {
System.out.println("-=-=-=-=-服务端启动-=-=-=-=-");
try {
// 1、创建ServerSocket对象,注册端口
ServerSocket serverSocket = new ServerSocket(6565);
// 2、调用ServerSocket对象的accept方法,等待客户端的Socket请求,建立Socket通信管道
Socket socket = serverSocket.accept();
// 3、从Socket通信管道中得到字节输入流
InputStream is = socket.getInputStream();
// 4、将低级的字节输入流转换成字符输入流,并包装成高级的缓冲字符输入流。
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、使用while死循环不断等待客户端发送消息,接收后展示。
String msg; // 定义变量,存储按照行读取到的消息。
while ( (msg = br.readLine()) != null ) {
System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3、测试
4、问题??
本案例实现了多发多收,那么是否可以同时接收多个客户端的消息??
- 不可以的。
- 为什么?
- 因为服务端现在只有一个线程,只能与一个客户端进行通信。
总结
1、本次多发多收是如何实现的?
- 客户端使用循环反复地发送消息。
- 服务端使用循环反复地接收消息。
2、现在服务端为什么不可以同时接收多个客户端的消息?
- 因为目前服务是单线程的,每次只能处理一个客户端发送的消息。
七、TCP通信案例:同时接收多个客户端消息【重点】
1、问题??
(1)之前我们的TCP通信是否可以同时与多个客户端通信,为什么??
- 不可以。
- 因为TCP通信的服务端是单线程,每次只能处理一个客户端的Socket通信。
(2)如何才可以让服务端可以处理多个客户端的通信需求?
- 引入多线程。
2、同时处理多个客户端消息
(1)思路模型
- 让主线程不断接收客户端的Socket连接;
- 将接收到的多个客户端的Socket连接交给独立的线程去处理。
(2)分析实现
- 客户端:
- 创建Socket对象,指定服务端的IP和端口,请求与服务端连接。
- 调用Socket对象的getOutputStream()方法,得到字节输出流。
- 将低级的字节输出流包装成高级的打印流。
- 创建键盘录入对象,用于客户端输入数据。
- 使用while死循环不断让客户端输入数据,如果输入的是exit说明客户端已离线!释放资源!
- 如果输入的不是exit,说明程序走到这里,则将客户端输入的数据发送给服务端。
- 每发送完一条数据,必须刷新数据。
package com.app.d8_tcp_socket3;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
需求:开发客户端可以不断的发送数据给服务端,输入exit说明离线。
*/
public class ClientDemo1 {
public static void main(String[] args) {
System.out.println("--==--==--客户端启动--==--==--");
try {
// 1、创建Socket对象,指定服务端的IP和端口,与服务端建立连接
Socket socket = new Socket("127.0.0.1", 8888);
// 2、使用Socket对象的getOutputStream()方法,得到字节输出流
OutputStream os = socket.getOutputStream();
// 3、将低级的字节输出流包装成高级的打印流,负责发送数据给服务端
PrintStream ps = new PrintStream(os);
// 4、创建键盘录入对象,用于客户端输入数据
Scanner sc = new Scanner(System.in);
// 5、使用while死循环不断让客户端输入数据
while (true) {
// 客户端开始输入数据
System.out.println("请说:");
String msg = sc.nextLine();
// 如果输入的是exit,则说明客户端已离线!释放资源
if (msg.equals("exit")) {
System.out.println("您已离线!!");
socket.close();
return;
}
// 6、如果输入的不是exit,说明程序走到这里,则将客户端输入的数据发送给服务端
ps.println(msg);
// 7、客户端每发送完一条数据,必须刷新数据。
ps.flush();
}
} catch (Exception e) { // 捕获异常!
e.printStackTrace();
}
}
}
- 服务端:
- 创建ServerSocket对象,注册端口。
- 使用while死循环由主线程负责不断接收多个客户端的Socket连接请求。
- 调用ServerSocket对象的accept方法,等待客户端的Socket连接请求,建立Socket通信管道。
- 创建线程任务类ServerReaderThread(服务端读取线程),重写run方法:
- 从Socket通信管道中得到字节输出流。
- 将低级的字节输入流转换成字符输入流,并包装成高级的缓冲字符输入流,负责接收客户端发来的数据。
- 使用while死循环不断按照行读取客户端发来的数据。
- 展示读取到的数据。
- 每接收到一个客户端的Socket连接请求,主线程将其交给独立的线程去处理。
package com.app.d8_tcp_socket3;
import java.net.ServerSocket;
import java.net.Socket;
/**
需求:开发服务端可以同时接收多个客户端发送过来的数据。
*/
public class ServerDemo2 {
public static void main(String[] args) {
System.out.println("--==--==--服务端启动--==--==--");
try {
// 1、创建ServerSocket对象,注册端口。
ServerSocket serverSocket = new ServerSocket(8888);
// 2、使用while死循环由主线程负责不断接收多个客户端发出的Socket连接请求
while (true) {
// 3、调用ServerSocket对象的accept方法,等待客户端的Socket连接请求,建立Socket通信管道。
Socket socket = serverSocket.accept();
// 5、每接收到一个客户端的Socket连接请求,主线程将其交给独立的线程去处理。
new ServerReaderThread(socket).start();
}
} catch (Exception e) { // 捕获异常!
e.printStackTrace();
}
}
}
package com.app.d8_tcp_socket3;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
/**
4、创建线程任务类:ServerReaderThread(服务端读取线程)
*/
public class ServerReaderThread extends Thread {
// 2、定义一个socket变量,用于存储主线程交过来的客户端的Socket连接请求
private Socket socket;
// 3、定义一个有参数构造器,用于将主线程交过来的客户端的Socket连接请求初始化到私有的socket变量
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
/**
1、重写run方法
*/
@Override
public void run() {
try {
// a.从Socket通信管道中得到字节输入流
InputStream is = socket.getInputStream();
// b.将低级的字节输入流转换成字符输入流,并包装成高级的缓冲字符输入流,负责读取接收客户端发送过来的数据。
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// c.使用while死循环不断按照行读取客户端发送过来的数据
String msg;
while ( (msg = br.readLine()) != null ) {
// d.展示读取到的数据
System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
}
} catch (Exception e) { // 捕获异常!
e.printStackTrace();
}
}
}
(3)测试
(4)拓展
- 跟踪某个客户端上线和下线。
- 跟踪上线逻辑:
- 直接在接收到客户端的Socket连接请求的地方,打印输出一个上线日志:
- 跟踪下线逻辑:
- 直接将输入exit的判断逻辑代码注释掉,然后在线程任务类的run方法中的捕获异常处做客户端下线提醒
- 测试:
总结
1、本次是如何实现服务端接收多个客户端的消息的?
- 主线程使用了
循环负责接收客户端Socket
管道连接。 - 每接收到一个Socket通信管道
分配一个独立的线程负责处理
。