【计算机网络】网络编程套接字之TCP套接字——含超详细代码注释

文章目录

  • TCP流套接字编程
    • ServerSocket API
      • ServerSocket 构造方法
      • ServerSocket 方法
    • Socket API
      • Socket 构造方法
      • Socket 方法
  • 案例及超详细注释
    • 案例一(回显服务)
    • 实例二(回显服务——多线程版本)
    • 实例三(线程池版本)

引言:

上篇文章我们一起学习了【计算机网络】网络编程套接字之UDP数据报套接字,今天让我们来一起继续学习 网络编程套接字之TCP套接字编程

TCP流套接字编程

ServerSocket API

ServerSocket 是创建TCP服务端Socket的API。

ServerSocket 构造方法

方法签名 方法说明
ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket 方法

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

Socket API

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

Socket 构造方法

方法签名 方法说明
Socket(String host, int port) 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

Socket 方法

方法签名 方法说明
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流

案例及超详细注释

案例一(回显服务)

服务器启动四步骤:

  1. 建立连接
  2. 读取客户端发来的请求
  3. 根据请求计算响应
  4. 把响应写回到客户端

看到这四个步骤,不知道有的小伙伴会不会有疑惑说,上篇文章UDP数据报套接字编程时,启动服务器只需要三个步骤啊,这怎么又多了个步骤捏❓❓❓这是因为UDP协议是无连接的,而TCP协议是有连接的,不能一上来就读取数据,而是要先建立连接,就好像我们平时打电话一样,我们要先建立连接,确保对方接通了才可以开始通话。

代码实现如下:
服务器端:

package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoServer {
    private ServerSocket serverSocket=null;
    public TcpEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动了!");
        while (true){
            //由于TCP是有连接的,不能一上来就读数据,而要先建立连接(像接电话一样)
            //accept就是在“接电话”,接电话的前提是,有人给你打了,如果当前没有客户端尝试建立连接,此处的accept就会阻塞
            //accept返回一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成的
            //进一步讲,serverSocket就干了一件事,接电话~
            Socket clientSocket=serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d]客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来处理请求和响应
        //这里的针对TCP socket的读写和文件读写一模一样
        try (InputStream inputStream=clientSocket.getInputStream()){
            try(OutputStream outputStream= clientSocket.getOutputStream()){
                //循环的处理每个请求,分别返回响应
                Scanner scanner=new Scanner(inputStream);
                while (true){
                    //1.读取请求
                    if (!scanner.hasNext()){
                        System.out.printf("[%s:%d]客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //此处用Scanner更方便,如果不用Scanner就用原生的InputStream的read也可以
                    String request=scanner.next();
                    //2.根据请求,计算响应
                    String response=process(request);
                    //3.把这个响应返回给客户端
                    //为了方便起见,可以使用PrintWriter把OutputStream包裹一下
                    PrintWriter printWriter=new PrintWriter(outputStream);
                    printWriter.println(response);
                    //刷新缓冲区,如果没有这个刷新,可能客户端就不能第一时间看到响应结果
                    printWriter.flush();
                    System.out.printf("[%s:%d]req:%s,resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                //记得关闭
                //由于clientSocket是每次连接都创建个新的,也就是数目很多,并且连接断开也就不再需要了,所以它会持续的进行积累
                //因此我们需要保证每次处理完的连接都要给释放了
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer=new TcpEchoServer(9090);
        tcpEchoServer.start();
    }
}

客户端:

package network;

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;

    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //这里传入的IP和端口号的含义表示的不是自己绑定,而是表示和这个IP端口建立连接!
        //调用这个构造方法,就会和服务器建立连接(打电话拨号了)
        socket = new Socket(serverIp,serverPort);
    }
    public void start(){
        System.out.println("和服务器连接成功了!");
        Scanner scanner=new Scanner(System.in);
        try(InputStream inputStream= socket.getInputStream()){
            try(OutputStream outputStream= socket.getOutputStream()){
                while (true){
                    //1.从控制台读取字符串
                    System.out.println("->");
                    String request=scanner.next();
                    //2.根据读取的字符串,构成请求,把请求发送给服务器
                    PrintWriter printWriter=new PrintWriter(outputStream);
                    printWriter.println(request);
                    printWriter.flush();
                    //3.从服务器读取响应,并解析
                    Scanner respScanner=new Scanner(inputStream);
                    String response=respScanner.next();
                    //4.把结果显示到控制台上
                    System.out.printf("req: %s,resp: %s\n",request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",9090);
        tcpEchoClient.start();
    }
}

【计算机网络】网络编程套接字之TCP套接字——含超详细代码注释_第1张图片
【计算机网络】网络编程套接字之TCP套接字——含超详细代码注释_第2张图片

虽然上面的TCP代码已经跑起来了,但是还存在一个很严重的问题❗❗那就是当前的服务器,在同一时刻只能处理一个连接,这就很不科学❗
那么为啥当前的服务器只能处理一个客户端嘞❓那是因为能够和客户端交互的前提是,要先调用accept,接收连接(也就是接通电话)图解如下⬇️⬇️⬇️

【计算机网络】网络编程套接字之TCP套接字——含超详细代码注释_第3张图片
当前这个问题就好像,你和别人在打电话,而此时其他人若再给你打电话,就没法继续接通了。
要想解决上述问题,就得让processConnection 的执行,和前面的accept的执行互相不干扰;不能让processConnection里面的循环导致accept无法及时调用。
所以此时就需要我们之前的老朋友隆重登场了,那就是——多线程

那么为啥UDP版本的程序就没用多线程,也是好着的呢❓❓
因为UDP不需要处理连接,UDP只要一个循环,就可以处理所有客户端的请求;
但是此处,TCP既要处理连接,又要处理一个连接中的若干次请求,就需要两个循环,里层循环就会影响到外层循环的进度了~
因此在主线程循环调用accept 时,当有客户端连接上来的时候,就直接让主线程创建一个新线程,由新线程负责对客户端的若干个请求,提供服务。(在新线程里通过while循环来处理请求) ,这个时候多个线程是并发执行的关系(宏观上看起来同时执行)。这样的话就是各自执行各自的了,就不会相互干扰了。

注意:每个客户端连上来都需要分配一个线程

实例二(回显服务——多线程版本)

【计算机网络】网络编程套接字之TCP套接字——含超详细代码注释_第4张图片
代码实现如下:
服务器端:

package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpThreadEchoServer {
    private ServerSocket serverSocket=null;
    public TcpThreadEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动了!");
        while (true){
            //由于TCP是有连接的,不能一上来就读数据,而要先建立连接(像接电话一样)
            //accept就是在“接电话”,接电话的前提是,有人给你打了,如果当前没有客户端尝试建立连接,此处的accept就会阻塞
            //accept返回一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成的
            //进一步讲,serverSocket就干了一件事,接电话~
            Socket clientSocket=serverSocket.accept();
            //改进方法:在这个地方,每次accept成功,都创建一个新的线程,由新线程负责执行这个processConnection方法
            Thread t=new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d]客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来处理请求和响应
        //这里的针对TCP socket的读写和文件读写一模一样
        try (InputStream inputStream=clientSocket.getInputStream()){
            try(OutputStream outputStream= clientSocket.getOutputStream()){
                //循环的处理每个请求,分别返回响应
                Scanner scanner=new Scanner(inputStream);
                while (true){
                    //1.读取请求
                    if (!scanner.hasNext()){
                        System.out.printf("[%s:%d]客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //此处用Scanner更方便,如果不用Scanner就用原生的InputStream的read也可以
                    String request=scanner.next();
                    //2.根据请求,计算响应
                    String response=process(request);
                    //3.把这个响应返回给客户端
                    //为了方便起见,可以使用PrintWriter把OutputStream包裹一下
                    PrintWriter printWriter=new PrintWriter(outputStream);
                    printWriter.println(response);
                    //刷新缓冲区,如果没有这个刷新,可能客户端就不能第一时间看到响应结果
                    printWriter.flush();
                    System.out.printf("[%s:%d]req:%s,resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                //记得关闭
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    private String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpThreadEchoServer tcpThreadEchoServer=new TcpThreadEchoServer(9090);
        tcpThreadEchoServer.start();
    }
}

客户端:

客户端代码和上述回显服务客户端代码一致

结果图如下:(启动了两个客户端)
【计算机网络】网络编程套接字之TCP套接字——含超详细代码注释_第5张图片

实例三(线程池版本)

package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpThreadPoolEchoServer {
    private ServerSocket serverSocket=null;
    public TcpThreadPoolEchoServer(int port) throws IOException {
        serverSocket=new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动了!");
        ExecutorService pool= Executors.newCachedThreadPool();
        while (true){
            //由于TCP是有连接的,不能一上来就读数据,而要先建立连接(像接电话一样)
            //accept就是在“接电话”,接电话的前提是,有人给你打了,如果当前没有客户端尝试建立连接,此处的accept就会阻塞
            //accept返回一个socket对象,称为clientSocket,后续和客户端之间的沟通,都是通过clientSocket来完成的
            //进一步讲,serverSocket就干了一件事,接电话~
            Socket clientSocket=serverSocket.accept();
            //利用线程池来实现
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });

        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d]客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //接下来处理请求和响应
        //这里的针对TCP socket的读写和文件读写一模一样
        try (InputStream inputStream=clientSocket.getInputStream()){
            try(OutputStream outputStream= clientSocket.getOutputStream()){
                //循环的处理每个请求,分别返回响应
                Scanner scanner=new Scanner(inputStream);
                while (true){
                    //1.读取请求
                    if (!scanner.hasNext()){
                        System.out.printf("[%s:%d]客户端断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //此处用Scanner更方便,如果不用Scanner就用原生的InputStream的read也可以
                    String request=scanner.next();
                    //2.根据请求,计算响应
                    String response=process(request);
                    //3.把这个响应返回给客户端
                    //为了方便起见,可以使用PrintWriter把OutputStream包裹一下
                    PrintWriter printWriter=new PrintWriter(outputStream);
                    printWriter.println(response);
                    //刷新缓冲区,如果没有这个刷新,可能客户端就不能第一时间看到响应结果
                    printWriter.flush();
                    System.out.printf("[%s:%d]req:%s,resp:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                //记得关闭
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    private String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpThreadPoolEchoServer tcpThreadPoolEchoServer=new TcpThreadPoolEchoServer(9090);
        tcpThreadPoolEchoServer.start();
    }
}

【计算机网络】网络编程套接字之TCP套接字——含超详细代码注释_第6张图片

你可能感兴趣的:(计算机网络,网络,tcp/ip,udp)