简单的服务器 TCP版本 熟悉其 API 【网络】

x33g5p2x  于2022-02-07 转载在 其他  
字(9.9k)|赞(0)|评价(0)|浏览(272)

前言: 上篇,我们写了 UDP 版本的简单服务器,本篇通过写一个简单的服务器来了解 TCP 的常用API

TCP 版本 (以长连接为例)

面向字节流

服务器程序

流程梳理:

  1. 初始化服务器
  2. 进入主循环
    2.1) 先从内核中获取到一个 TCP 连接
    2.2) 处理 TCP 连接
    a) 读取请求并解析
    b) 根据请求计算响应
    c) 把响应结果写到客户端

仍以简单的 echo sever 为例

1.初始化操作

  1. // 创建一个 socket 对象
  2. private ServerSocket serverSocket = null;
  3. public TcpEchoServer(int port) throws IOException {
  4. // 和 UDP 类似,绑定端口号
  5. serverSocket = new ServerSocket(port);
  6. }

2.1 从内核中获取到一个 TCP 连接

  1. Socket clientSocket = serverSocket.accept();

TCP 连接管理是由操作系统内核来管理的
所谓的连接管理:先描述(通信中的五元组),再组织(使用一个阻塞队列来组织若干个对象)
.
客户端和服务器建立连接的过程,完全由内核来进行负责,应用程序的代码感知不到
当连接建立成功,此时内核已经把这个连接对象放到阻塞队列中了
.
代码中调用的 accept: 就是从阻塞队列中取出一个连接对象(在应用程序中的化身就是 Socket 对象)
若服务器启动之后,没有客户端进行连接,此时代码中调用 accept 就会阻塞,阻塞到真的有客户端建立连接为止

2.2 处理 TCP 连接

  1. processConnection(clientSocket);
  2. private void processConnection(Socket clientSocket) {
  3. System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress().toString(),
  4. clientSocket.getPort());
  5. // 通过 clientSocket 来和客户端交互,先做好准备工作, 获取到 clientSocket 中的流对象
  6. try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
  7. BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
  8. } catch (IOException ioException) {
  9. ioException.printStackTrace();
  10. }
  11. }

clientSocket 里有 getInputStream( ) 方法,和 getOutputStream( ) 方法
此处得到的两个对象都是 “字节流
InputStream,用来进行读取
OutputStream,用来进行写入
.

  1. private void processConnection(Socket clientSocket) {
  2. System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress().toString(),
  3. clientSocket.getPort());
  4. // 通过 clientSocket 来和客户端交互,先做好准备工作, 获取到 clientSocket 中的流对象
  5. try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
  6. BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
  7. // 此处实现一个 "长连接" 版本的服务器
  8. // 一次连接的处理过程中需要处理多个请求和响应
  9. // 当客户端断开连接时,服务器再调用 readLine 或 write都会触发异常 (IOException),循环结束
  10. while (true){
  11. // a)读取请求并解析
  12. String request = bufferedReader.readLine(); // 客户端发的数据必须是一个按行发送的数据(每一条数据占一行)
  13. // b)根据请求计算响应
  14. String response = process(request);
  15. // c)把响应写回客户端
  16. bufferedWriter.write(response + "\n"); // 因为客户端要按行来读
  17. bufferedWriter.flush(); // 刷新缓冲区
  18. // 打印日志
  19. System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
  20. clientSocket.getPort(),request,response);
  21. }
  22. } catch (IOException e) {
  23. System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress().toString(),
  24. clientSocket.getPort());
  25. }
  26. }

附完整代码:

  1. public class TcpEchoServer {
  2. // 创建一个 socket 对象
  3. private ServerSocket serverSocket = null;
  4. public TcpEchoServer(int port) throws IOException {
  5. // 和 UDP 类似,绑定端口号
  6. serverSocket = new ServerSocket(port);
  7. }
  8. public void start() throws IOException {
  9. System.out.println("服务器启动..");
  10. while (true){
  11. // 2.1) 先从内核中获取到一个 TCP 连接
  12. Socket clientSocket = serverSocket.accept();
  13. // 2.2) 处理 TCP 连接
  14. processConnection(clientSocket);
  15. }
  16. }
  17. private void processConnection(Socket clientSocket) {
  18. System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress().toString(),
  19. clientSocket.getPort());
  20. // 通过 clientSocket 来和客户端交互,先做好准备工作, 获取到 clientSocket 中的流对象
  21. try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
  22. BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
  23. // 此处实现一个 "长连接" 版本的服务器
  24. // 一次连接的处理过程中需要处理多个请求和响应
  25. // 当客户端断开连接时,服务器再调用 readLine 或 write都会触发异常 (IOException),循环结束
  26. while (true){
  27. // a)读取请求并解析
  28. String request = bufferedReader.readLine(); // 客户端发的数据必须是一个按行发送的数据(每一条数据占一行)
  29. // b)根据请求计算响应
  30. String response = process(request);
  31. // c)把响应写回客户端
  32. bufferedWriter.write(response + "\n"); // 因为客户端要按行来读
  33. bufferedWriter.flush(); // 刷新缓冲区
  34. // 打印日志
  35. System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
  36. clientSocket.getPort(),request,response);
  37. }
  38. } catch (IOException e) {
  39. System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress().toString(),
  40. clientSocket.getPort());
  41. }
  42. }
  43. public String process(String request) {
  44. // 回显服务器
  45. return request;
  46. }
  47. public static void main(String[] args) throws IOException {
  48. TcpEchoServer server = new TcpEchoServer(6060);
  49. server.start();
  50. }
  51. }

服务器的处理方式:

TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接

短连接

一个连接中,客户端和服务器之间只交互一次,交互完毕,就断开连接
每次接收到数据并返回响应后,都关闭连接;也就是说,短连接只能一次收发数据

长连接

一个连接中,客户端和服务器之间交互 N 次,直到满足一定条件再断开连接
不关闭连接,一直保持连接状态,双方不停的收发数据;也就是说,长连接可以多次收发数据
一般认为,长连接效率更高,避免了反复建立连接和断开连接的过程

客户端程序

核心流程:

  1. 启动客户端(一定不要绑定端口号),和服务器建立连接
  2. 进入主循环
    a)读取用户输入内容
    b)构造一个请求发送给服务器
    c)读取服务器响应数据
    d)把响应数据显示到界面上
  1. private Socket socket = null;
  2. public TcpEchoClient(String serverIP,int serverPort) throws IOException {
  3. // 此处实例化 socket 过程,就是在建立 TCP连接
  4. socket = new Socket(serverIP,serverPort);
  5. }

a) 读取用户输入内容

  1. System.out.print("-> ");
  2. String request = scan.nextLine();
  3. if(request.equals("exit")){
  4. break;
  5. }

此处的读操作也会阻塞 (用户不一定立刻就输入) —— 阻塞等待用户输入数据
若发生生阻塞,则一直阻塞到用户真的输入了数据为止

b) 构造一个请求发送给服务器

  1. bufferedWriter.write(request + "\n");
  2. // "\n" 为了和服务器中的 readLine 相对应
  3. bufferedWriter.flush(); // 刷新缓冲区

此处的 write 发送成功后,服务器就会从 readLine 中返回
**注意:**有缓冲区的 IO操作,真正传输数据,需要刷新缓冲区

c) 读取服务器响应数据

  1. String response = bufferedReader.readLine();

在服务器返回数据之前,此处的 readLine 也会阻塞 —— 阻塞等待服务器返回响应

d) 把响应数据显示到界面上

  1. System.out.println(response);

完整代码:

  1. public class TcpEchoClient {
  2. private Socket socket = null;
  3. public TcpEchoClient(String serverIP,int serverPort) throws IOException {
  4. // 此处实例化 socket 过程,就是在建立 TCP连接
  5. socket = new Socket(serverIP,serverPort);
  6. }
  7. public void start(){
  8. System.out.println("客户端启动...");
  9. Scanner scan = new Scanner(System.in);
  10. try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  11. BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
  12. while (true){
  13. // a)读取用户输入内容
  14. System.out.print("-> ");
  15. String request = scan.nextLine();
  16. if(request.equals("exit")){
  17. break;
  18. }
  19. // b)构造一个请求发送给服务器
  20. bufferedWriter.write(request + "\n"); // "\n" 为了和服务器中的 readLine 相对应
  21. bufferedWriter.flush(); // 刷新缓冲区
  22. // c)读取服务器响应数据
  23. String response = bufferedReader.readLine();
  24. // d)把响应数据显示到界面上
  25. System.out.println(response);
  26. }
  27. } catch (IOException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. public static void main(String[] args) throws IOException {
  32. TcpEchoClient client = new TcpEchoClient("127.0.0.1",6060);
  33. client.start();
  34. }
  35. }

多线程改进服务器

上述代码:是一个服务器只能给一个客户端服务,那这个服务器太low了,可以使用多线程对其改进:

代码实现:

  1. public class TcpThreadEchoServer {
  2. private ServerSocket serverSocket = null;
  3. public TcpThreadEchoServer(int port) throws IOException {
  4. serverSocket = new ServerSocket(port);
  5. }
  6. public void start() throws IOException {
  7. System.out.println("服务器启动..");
  8. // 此处的 while 可以反复快速的调用到 accept ,于是就可以同时处理多个客户端的连接了
  9. while (true){
  10. Socket clientSocket = serverSocket.accept();
  11. // 针对这个连接,单独创建一个线程进行处理
  12. Thread t = new Thread(){
  13. @Override
  14. public void run(){
  15. // accept 和 processConnection 是并发执行的
  16. processConnection(clientSocket);
  17. }
  18. };
  19. t.start();
  20. }
  21. }
  22. public void processConnection(Socket clientSocket) {
  23. System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),
  24. clientSocket.getPort());
  25. try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
  26. BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
  27. while (true){
  28. // a)读取请求并解析
  29. String request = bufferedReader.readLine(); // 客户端发的数据必须是一个按行发送的数据(每一条数据占一行)
  30. // b)根据请求计算响应
  31. String response = process(request);
  32. // c)把响应写回客户端
  33. bufferedWriter.write(response + "\n"); // 因为客户端要按行来读
  34. bufferedWriter.flush(); // 刷新缓冲区
  35. // 打印日志
  36. System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
  37. clientSocket.getPort(),request,response);
  38. }
  39. } catch (IOException e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. public String process(String request) {
  44. return request;
  45. }
  46. public static void main(String[] args) throws IOException {
  47. TcpThreadEchoServer server = new TcpThreadEchoServer(6060);
  48. server.start();
  49. }
  50. }

先接收请求,再分配线程
主线程专门负责 accept,其他线程负责和客户端具体沟通

虽然使用多线程解决了 “一个服务器只能给一个客户端服务” 的问题
但是,每次来一个客户端,都要分配一个线程,对于一个服务器来说,随时可能会来大量的客户端,随时也会有大量的客户端断开连接,那么服务器也就需要频繁的创建和销毁线程 (线程的创建和销毁比较轻量,但是也"来不住"量大)
那么,就可以使用 线程池 来解决

线程池再改进:

通过使用线程池,就可以节省频繁创建和销毁线程带来的开销

  1. public class TcpThreadPoolEchoServer {
  2. private ServerSocket serverSocket = null;
  3. public TcpThreadPoolEchoServer(int port) throws IOException {
  4. serverSocket = new ServerSocket(port);
  5. }
  6. public void start() throws IOException {
  7. System.out.println("服务器启动..");
  8. // 创建一个线程池实例
  9. ExecutorService executorService = Executors.newCachedThreadPool();
  10. // 此处的 while 就可以反复快速的调用到 accept ,于是就可以同时处理多个客户端的连接了
  11. while (true){
  12. Socket clientSocket = serverSocket.accept();
  13. // 针对这个连接,单独创建一个线程进行处理
  14. executorService.execute(new Runnable() {
  15. @Override
  16. public void run() {
  17. processConnection(clientSocket);
  18. }
  19. });
  20. }
  21. }
  22. public void processConnection(Socket clientSocket) {
  23. System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),
  24. clientSocket.getPort());
  25. try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
  26. BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
  27. while (true){
  28. // a)读取请求并解析
  29. String request = bufferedReader.readLine(); // 客户端发的数据必须是一个按行发送的数据(每一条数据占一行)
  30. // b)根据请求计算响应
  31. String response = process(request);
  32. // c)把响应写回客户端
  33. bufferedWriter.write(response + "\n"); // 因为客户端要按行来读
  34. bufferedWriter.flush(); // 刷新缓冲区
  35. // 打印日志
  36. System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
  37. clientSocket.getPort(),request,response);
  38. }
  39. } catch (IOException e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. public String process(String request) {
  44. return request;
  45. }
  46. public static void main(String[] args) throws IOException {
  47. TcpThreadPoolEchoServer server = new TcpThreadPoolEchoServer(6060);
  48. server.start();
  49. }
  50. }

总结

方法总结:

  1. ServerSocket API

ServerSocket 是创建TCP服务端Socket的API

  • ServerSocket 构造方法
方法签名方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口
  • ServerSocket 方法:
方法签名方法说明
Socketaccept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close( )关闭此套接字
  1. Socket API

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端
Socket
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据

  • Socket 构造方法
方法签名方法说明
Socket(String host, intport)创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接
  • Socket 方法:
方法签名方法说明
InetAddress getInetAddress( )返回套接字所连接的地址
InputStream getInputStream( )返回套接字所连接的输入流
OutputStream getOutputStream( )返回此套接字的输出流

UDP & TCP 区别

UDP 核心类TCP 核心类
①DatagramSocket①ServerSocket
②DatagramPacket②Socket

在UDP中,必须使用 receive 和 send,传输数据的基本单位是 DatagramPacket 对象 【面向数据报】
在TCP中,必须要 read 和 write,传输数据的基本单位是 字节 【面向字节流】

UDPTCP
是否连接无连接面向连接
是否可靠不可靠传输,不使用流量控制和拥塞控制可靠传输,使用流量控制和拥塞控制
传输方式面向数据报面向字节流

相关文章