目录
一. TCP API
二. TCP回显服务器-客户端
1. 服务器
2. 客户端
3. 服务端-客户端工作流程
4. 服务器优化
TCP数据流套接字编程是一种基于有连接协议的网络通信方式
一. TCP API
在TCP编程中,主要使用两个核心类ServerSocket 和 Socket
ServerSocket
ServerSocket类只有服务器会使用,用于接受连接
ServerSocket构造方法:
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket方法:
方法签名 | 方法说明 |
---|---|
Socket accept() | 监听端口,如果有客户端连接后,则会返回一个服务端 Socket 对象 如果没有客户端连接,则会进入阻塞等待 |
void close() | 关闭此套接字 |
- accept()方法用于接收客户端连接请求并建立正式通信通道
- accept()方法是接受连接并返回Socket,真正和客户端进行交互的是Socket
Socket
Socket类负责具体的数据传输
- 客户端一开始就使用Socket进行通信(请求由客户端发起)
- 服务器在接受客户端建立请求后,返回服务端Socket
- 在双方建立连接之后,都会使用Socket进行通信
Socket 构造方法:
方法签名 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket 方法:
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回的地址(IP和端口) |
InputStream getInputStream() | 返回输入流 |
OutputStream getOutputStream() | 返回输出流 |
- TCP面向字节流,基本传输单位是字节
二. TCP回显服务器-客户端
回显服务器
回显服务器:不进行任何的业务逻辑,只是将收到的数据显示出来
1. 服务器
接收连接请求
- TCP是有连接的可靠通信
- 真正建立连接的过程在内核中被实现,应用层只是调用相应API同意建立连接
- 类比打电话,客户端拨号,服务器这边在响铃,通过调用accept接听
代码实现:
Socket clientSocket = serverSocket.accept();
- accept()方法具有阻塞功能
- accept()方法一次只能返回一个Socket对象,接收一次请求
- 如果没有客户端发起连接请求,则会进入阻塞等待
- 如果有一个客户端发起连接请求,则执行一次,如果有多个客户端发起连接请求,则执行多次
处理请求
private void processConnection(Socket clientSocket) {}
- 使用方法专门处理一次连接,在一次连接中可能会涉及多次请求响应交互
如何处理请求和返回响应?
由于TCP面向字节流,我们可以字节流传输的类 InputStream和OutputStream
try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){} catch (IOException e) {throw new RuntimeException(e);}
- 使用try-with-resources管理InputStream和OutputStream,确保流自动关闭。
- InputStream从网卡中读取数据
- OutputStream从网卡中写入数据
1)接收请求并解析
Scanner scanner = new Scanner(inputStream);if(!scanner.hasNext()){System.out.printf("[客户端ip:%s,端口号:%d],客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}String request = scanner.next();
- 客户端和服务器双方都有自己的缓冲区
- 客户端发送数据,会先将数据放入服务器缓冲区中
- 如果服务器缓冲区中没有数据,hasNext()则会陷入阻塞等待中
- 如果客户端退出,则会触发四次挥手断开连接,服务器会感知到,就会在hasNext()返回false。
2)根据请求计算响应
回显服务器:不会处理数据,输入什么就会返回什么
String response = process(request);
使用process方法来实现回显功能
public String process(String request) {return request;}
如果想要实现特定的功能,直接在process中实现即可
3)返回响应
Scanner的写操作无法自己完成,只能进行读取操作,写操作需要依靠其他的类(PrintWriter)
PrintWriter printWriter = new PrintWriter(outputStream);
// 将数据写入数据的缓冲区中printWriter.println(respond);
冲刷缓冲区
由于缓冲区的特殊机制,缓冲区只有满的时候,才会被发送出去
printWriter.flush();
- 我们这里要保证实时性,客户端每发送一次请求,服务器都要第一时间响应
- IO操作比较低效,如果每进行一次IO,就要冲刷一次,效率很低,为了让这种低效的操作少一点,等缓冲区满了,才会冲刷
服务器总代码:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TcpEchoServer {private ServerSocket serverSocket = null;TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("服务器启动");while(true){//从缓冲区内取出并同意链接//将取出的数据使用clientSocket另外保存起来,//每有一个客户端,就会出现一个clientSocket对象,所有使用完,必须关闭Socket clientSocket = serverSocket.accept();//进行数据分析/* Thread t = new Thread(()->{processConnection(clientSocket);});t.start();*/
// 这样写开销大,会有很多次的创建和销毁,改进使用线程池ExecutorService service = Executors.newFixedThreadPool(3);service.submit(()->{processConnection(clientSocket);});}}//使用这个方法专门处理一次连接,在一次连接中可能会涉及多次请求交互private void processConnection(Socket clientSocket) {System.out.printf("[客户端ip:%s,端口号:%d],客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());//循环处理请求并返回响应(请求可能不止一次)//从网卡中读数据和写数据try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){while (true){
// byte[] buffer = new byte[1024];
// int n = inputStream.read(buffer);
// //将字节数组转换为字符串
// if(n==-1){
// System.out.printf("[客户端ip:%s,端口号:%d],客户端下线",clientSocket.getInetAddress(),clientSocket.getPort());
// break;
// }
// String request = new String(buffer,0,n);Scanner scanner = new Scanner(inputStream);if(!scanner.hasNext()){System.out.printf("[客户端ip:%s,端口号:%d],客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}
// 1.接受请求并解析//客户端必须有一个空格或者换行符String request = scanner.next();
// 2.根据请求计算响应String respond = process(request);
// 3.返回响应//返回的是字节数组类型
// outputStream.write(request.getBytes(),0,request.getBytes().length);//返回字符串类型(各种类型)PrintWriter printWriter = new PrintWriter(outputStream);
// 将数据写入数据的缓冲区中printWriter.println(respond);//冲刷缓冲区printWriter.flush();//打印日志System.out.printf("[客户端ip:%s,端口号:%d],req:%s,resp:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,respond);}} catch (IOException e) {throw new RuntimeException(e);}finally {try {//必须进行close,clientSocket.close();} catch (IOException e) {throw new RuntimeException(e);}}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(9090);server.start();}
}
注意:
- 在服务器中,ServerSocket对象不需要被消耗,整个程序中只有一个ServerSocket对象,它的生命周期要伴随整个程序,不能提前关闭,只有程序退出了,才会被释放
- 方法中的Socket必须要释放,每出现一个客户端,就会随之出现一个Socket对象,如果不释放,Socket对象会越来越多,将文件描述符表占满(内存泄露问题)
2. 客户端
构造方法
TcpEchoClient(String serverIp,int serverPort) throws IOException {socket = new Socket(serverIp,serverPort);}
- 这里不需要将serverIP和serverPort在类中保存
- 因为tcp有链接,socket会保存好这两个值
客户端如何发送请求和接收响应?
客户端同样使用字节流进行传输
try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){} catch (IOException e) {throw new RuntimeException(e);}
- 使用try-with-resources管理InputStream和OutputStream,确保流自动关闭。
- InputStream从网卡中读取数据
- OutputStream从网卡中写入数据
1)从控制台读取请求
//客户端输入的数据Scanner scannerConsole = new Scanner(System.in);while(true){System.out.print("->");//客户端没有输入if(!scannerConsole.hasNext()){break;}
// 从控制台读取请求String request = scannerConsole.next();}
- 使用Scanner进行输入,如果没有输入数据,hasNext()会进入阻塞等待
2)将请求发送给服务器
Scanner只会读取数据,发送使用类PrintWriter
PrintWriter writer = new PrintWriter(outputStream);writer.println(request);
- 向服务器发送数据
冲刷缓冲区
//冲刷缓冲区writer.flush();
将发送的数据,先放入缓冲区中,等待缓冲区满了,才会发送缓冲区中的内容
3)接收服务器返回的响应
Scanner scannerNetwork = new Scanner(inputStream);String respond = scannerNetwork.next();
- 服务器发送的数据,先到达客户端的缓冲区,客户端要从缓冲区读出数据
- 这里使用Scanner进行读出数据,也可以使用read()方法读取
4)将响应数据显示在控制台
System.out.println(respond);
将接收到的字符串响应,直接打印出来即可
客户端总代码:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;public class TcpEchoClient {private Socket socket = null;TcpEchoClient(String serverIp,int serverPort) throws IOException {
// 这里不需要将serverIP和serverPort在类中保存
// 因为tcp有链接,socket会保存好这两个值socket = new Socket(serverIp,serverPort);}public void start(){System.out.println("客户端启动");try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){//客户端输入的数据Scanner scannerConsole = new Scanner(System.in);//通过网络读取Scanner scannerNetwork = new Scanner(inputStream);//像服务器发送请求PrintWriter writer = new PrintWriter(outputStream);while(true){System.out.print("->");//客户端没有输入if(!scannerConsole.hasNext()){break;}
// 1. 从控制台读取请求String request = scannerConsole.next();
// 2.将请求发送给服务端writer.println(request);//冲刷缓冲区writer.flush();
// 3.接受服务端返回的响应//从数据缓冲区中读取出内容String respond = scannerNetwork.next();
// 4.将响应显示在控制台System.out.println(respond);}} catch (IOException e) {throw new RuntimeException(e);}}public static void main(String[] args) throws IOException {TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);client.start();}
}
3. 服务端-客户端工作流程
无论是TCP还是UDP都是服务端先启动
创建连接过程
- 服务器启动,由于没有客户端建立连接,accept()进入阻塞,等待客户端创建连接
- 客户端启动,客户端申请和服务器建立连接
- 服务器从accept()阻塞中返回,调用processConnection()方法进行交互
双方交互过程
- 服务器进入processConnection()方法,执行到hasNext(),由于客户端没有发送数据,服务器读取不到数据,进入阻塞状态
- 客户端在hasNext()这里进入阻塞,等待用户在控制台中输入数据
- 用户输入数据,客户端从hasNext()中退出阻塞,将数据发送给服务器,next()阻塞等待服务器返回数据
- 服务器从hasNext()阻塞中返回,读取请求并处理,构造响应,发送给客户端
- 客户端读取响应并打印
4. 服务器优化
Thread t = new Thread(()->{processConnection(clientSocket);});t.start();
- 每来一个客户端,服务器就需要创建出一个新的线程
- 每次客户端结束,服务器就需要销毁这个线程
如果客户端比较多,那么服务器就需要频繁的创建和销毁 ,开销大
(1)可以通过引入线程池来避免频繁的创建和销毁
ExecutorService service = Executors.newFixedThreadPool(3);service.submit(()->{processConnection(clientSocket);});
如果有的客户端处理的过程很短(网站),也有可能客户端处理的时间会很长
处理时间很短的客户端,分配一个专门的线程,有点浪费,所有引入了IO多路复用技术
(2)IO多路复用技术
IO多路复用技术是操作系统提供的机制。
让一个线程去同时去负责处理多个Socket对象
本质在于这些Socket对象不是同一时刻都需要处理
虽然有多个Socket对象,但是同一时间活跃的Socket对象只是少数(大部分的Socket对象都是在等数据),我们可以在等的过程中,去处理活跃的Socket对象
点赞的宝子今晚自动触发「躺赢锦鲤」buff!