之前我们已经介绍了UDP套接字流程,接下来我们介绍TCP流套接字编程,TCP的一个核心特点,面向字节流,读写数据的基本单位就是字节。
1.API介绍
1.1ServerSocket:是创建TCP服务器Socket的API(专门给服务器用);
方法:
(服务器启动需要先绑定端口号)
TCP是“有连接”,这里的accept是联通连接的关键操作。
2.Socket
Socket是客户端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服 务端Socket(服务器和客户端都可以用)。
方法:
这两个参数是服务器的IP和服务器的端口。
代码实例:
TcpEchoServer
public class TcpEchoServer {private ServerSocket serverSocket = null;// 这里和 UDP 服务器类似, 也是在构造对象的时候, 绑定端口号.public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("启动服务器");// 这种情况一般不会使用 fixedThreadPool, 意味着同时处理的客户端连接数目就固定了.ExecutorService executorService = Executors.newCachedThreadPool();while (true) {// tcp 来说, 需要先处理客户端发来的连接.// 通过读写 clientSocket, 和客户端进行通信.// 如果没有客户端发起连接, 此时 accept 就会阻塞.// 主线程负责进行 accept, 每次 accept 到一个客户端, 就创建一个线程, 由新线程负责处理客户端的请求.Socket clientSocket = serverSocket.accept();// 使用多线程的方式来调整
// Thread t = new Thread(() -> {
// processConnection(clientSocket);
// });
// t.start();// 使用线程池来调整executorService.submit(() -> {processConnection(clientSocket);});}}// 处理一个客户端的连接.// 可能要涉及到多个客户端的请求和响应.private void processConnection(Socket clientSocket) {System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());try (InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {// 针对 InputStream 套了一层Scanner scanner = new Scanner(inputStream);// 针对 OutputStream 套了一层PrintWriter writer = new PrintWriter(outputStream);// 分成三个步骤while (true) {// 1. 读取请求并解析. 可以直接 read, 也可以借助 Scanner 来辅助完成.if (!scanner.hasNext()) {// 连接断开了System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());break;}String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 返回响应到客户端// outputStream.write(response.getBytes());writer.println(response);writer.flush();// 打印日志System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),request, response);}} catch (IOException e) {throw new RuntimeException(e);} finally {try {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();}
}
服务器引⼊多线程, 如果只是单个线程,⽆法同时响应多个客⼾端,此处给每个客⼾端都分配⼀个线程.
// 启动服务器
public void start() throws IOException {System.out.println("服务器启动!");while (true) {Socket clientSocket = serverSocket.accept();Thread t = new Thread(() -> {processConnection(clientSocket);});t.start();}
}
服务器引⼊线程池 为了避免频繁创建销毁线程,也可以引⼊线程池.
// 启动服务器
public void start() throws IOException {System.out.println("服务器启动!");ExecutorService service = Executors.newCachedThreadPool();while (true) {Socket clientSocket = serverSocket.accept();// 使⽤线程池, 来解决上述问题 service.submit(new Runnable() {@Overridepublic void run() {processConnection(clientSocket);}});}
}
TCPEchoClient
public class TcpEchoClient {private Socket socket = null;public TcpEchoClient(String serverIp, int serverPort) throws IOException {// 直接把字符串的 IP 地址, 设置进来.// 127.0.0.1 这种字符串socket = new Socket(serverIp, serverPort);}public void start() {Scanner scanner = new Scanner(System.in);try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {// 为了使用方便, 套壳操作Scanner scannerNet = new Scanner(inputStream);PrintWriter writer = new PrintWriter(outputStream);// 从控制台读取请求, 发送给服务器.while (true) {// 1. 从控制台读取用户输入String request = scanner.next();// 2. 发送给服务器writer.println(request);// 加上刷新缓冲区操作, 才是真正发送数据writer.flush();// 3. 读取服务器返回的响应.String response = scannerNet.next();// 4. 打印到控制台System.out.println(response);}} 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();}
}
1. 创建socket对象就会在底层和对端建立TCP连接,及记录了对端的信息(服务器的IP和端口),不需要自己在创建变量保存了,直接TCP内部就保存了。
2.
这两个socket对象不是同一个对象(它们在不同的进程中,甚至在不同的主机上)。
3.行为是自动加上\n
这个操作只是把数据放到“发送缓冲区”中,还没有真正写入到网卡中;如果这里使用print,则数据发过去,服务器收到了,但没有真正处理。
暗含一个约定,一个请求/响应,使用\n作为结束标记,对端读的时候,也是读到\n就结束(认为读到一个完整的请求)。
4.flush方法来“冲刷缓冲区”,才算真正的发送数据。
5.
判断收到的数据中是否包含“空白符”,遇到空白符(比如换行、回车、空格、制表符、翻页符...),认为一个“完整的next”,遇到之前都会堵塞。
6.
在这里clientSocket的生命周期是伴随一次连接的,每个客户端连接,都会创建一个新的;每个客户端断开连接,这个对象也就可以不要了,这时就需要加一个close(),防止文件的泄露。
TCP发送数据时,需要先建⽴连接,什么时候关闭连接就决定是短连接还是⻓连接:
- 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能⼀次收发 数据。
- ⻓连接:不关闭连接,⼀直保持连接状态,双⽅不停的收发数据,即是⻓连接。也就是说,⻓连接可 以多次收发数据。
对⽐以上⻓短连接,两者区别如下:
- 建⽴连接、关闭连接的耗时:短连接每次请求、响应都需要建⽴连接,关闭连接;⽽⻓连接只需要 第⼀次建⽴连接,之后的请求、响应都可以直接传输。相对来说建⽴连接,关闭连接也是要耗时 的,⻓连接效率更⾼。
- 主动发送请求不同:短连接⼀般是客⼾端主动向服务端发送请求;⽽⻓连接可以是客⼾端主动发送 请求,也可以是服务端主动发。
- 两者的使⽤场景有不同:短连接适⽤于客⼾端请求频率不⾼的场景,如浏览⽹⻚等。⻓连接适⽤于 客⼾端与服务端通信频繁的场景,如聊天室,实时游戏等。