【网络编程】TCP

请添加图片描述

✨个人主页:bit me
✨当前专栏:Java EE初阶

目 录

  • 一. TCP流套接字编程
  • 二. TCP中的长短连接
  • 三. 写一个 TCP 版本的 回显服务器-客户端

一. TCP流套接字编程

  • ServerSocket API

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

ServerSocket:是服务器端使用的 Socket

ServerSocket 构造方法:

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

ServerSocket 方法:

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

accept:没有参数,返回值是一个 Socket 对象,功能是等待有客户端和服务器建立上连接,accept 则会把这个连接获取到进程中,进一步的通过返回值的 Socket 对象来和客户端进行交互。

在此处可以举例为售楼,比如外场有男生拉客人,女生内场介绍售楼服务,其中 ServerSocket 就是外场连接,通过 accept 把连接交给了 Socket ,然后 Socket 对象和客户端进行沟通。

  • Socket API

Socket:服务器和客户端都会使用的 Socket 。

Socket 是客户端 Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。

不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

Socket 构造方法:

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

Socket 方法:

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

第二和第三个方法的传输数据,不是直接通过 Socket 对象,而是 Socket 内部包含了输入流对象(接收)输出流对象(发送)


二. TCP中的长短连接

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

  • 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
  • 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。

对比以上长短连接,两者区别如下:

  1. 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
  2. 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
  3. 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。

三. 写一个 TCP 版本的 回显服务器-客户端

服务器启动的时候,进行 accept ,accept 进行的工作是拉客,对于操作系统来说,建立 TCP 连接,是内核工作,accept 要干的就是等连接建立好了之后把这个连接拿到应用程序中,如果当前连接还没建立,accept 就会阻塞!accept 相当于,有人给你打电话了,你按下接听键!如果没人打电话,阻塞等待有人给你真的打电话了才行。

如果服务器返回多次响应,其实这多次响应,都是以字节为单位返回给客户端的,客户端操作系统内核就会收到这些字节,然后调用 read / scanner.next 这样的方法的时候,就是从内核中读取。

  • read :如果使用字节流的 read ,就是在读取固定长度的字节。即使返回 10 次,每次返回 10 个字节,这里 read (buf[100]),这种操作一次性就读出来了,也可以多次读取
  • scanner.next :隐含的东西,这里的 next 是读到空白符(空格,回车,换行,制表,翻页,垂直制表…)

测试网络程序的时候,务必要先启动服务器,再启动客户端。

客户端代码:

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() throws IOException {
        //new 这个对象,是要和服务器建立连接的
        //建立连接,是要知道服务器在哪里
        socket = new Socket("127.0.0.1",8000);
    }

    public void start() throws IOException {
        //由于我们建立的是长连接,一个连接会处理 N 个请求和响应
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true){
                //从控制台读取用户输入
                System.out.println("> ");
                String request = scanner.next();
                //2. 把请求发送给服务器
                printWriter.write(request);
                printWriter.flush();
                //3. 从服务器读取响应
                String response = scannerNet.next();
                //4. 把结果显示到界面上
                System.out.printf("req : %s; resp : %s\n",request,response);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient();
        client.start();
    }
}

服务器代码:

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){
            //如果当前没有客户端来进行连接,就会阻塞等待
            Socket clientSocket = serverSocket.accept();
            processConnect(clientSocket);
        }
    }

    //通过这个方法,给当前连上的这个客户端,提供服务
    //一个连接过来了,服务方式可能有两种
    //1. 一个连接只进行一次数据交互(一个请求 + 一个响应)  短连接
    //2. 一个连接进行多次数据交互(N 个请求 + N 个响应)   长连接
    //此处来写长连接版本
    public void processConnect(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            //这里是长连接的写法,需要用循环来获取到多次交互的情况
            while (true){
                if(!scanner.hasNext()){
                    //断开连接,当客户端断开连接的时候,此时 hasNext 就会返回 false
                    System.out.printf("[%s:%d] 断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //1. 读取请求并解析
                String request = scanner.next();
                //2. 根据请求计算响应
                String response = process(request);
                //3. 把响应返回给客户端
                printWriter.write(response);
                // 刷新一下缓冲区,避免数据没有发出去
                printWriter.flush();
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),
                        request,response);
            }
        }
    }
    public String process(String req){
        return req;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(8000);
        server.start();
    }
}

运行效果:

【网络编程】TCP_第1张图片
【网络编程】TCP_第2张图片

但是在此处我们发现客户端和服务器并没有想我们之前那样连接起来,输入内容之后,没发生反应,有两种可能性:

  1. 客户端没发出去(很可能没加 flush)
  2. 服务器收到了没有处理(很可能服务器还在阻塞的状态)

经过排查,猜测上面代码问题应该出现在服务器中,但是我们如何确认阻塞在哪里了呢?

我之前写过的,线程出现问题,可以查看 jconsole 查看当前线程状况,看到每个线程的调用栈

客户端中:

【网络编程】TCP_第3张图片

服务器中:

【网络编程】TCP_第4张图片

发现都是在 next 中阻塞了,下面两行就是代码阻塞的位置。

但是客户端阻塞在这里,是很合理的!等待服务器返回响应,服务器当前确实没有返回响应(如果服务器返回响应了,服务器会打印的,实际上服务器没打印这个日志)

next 啥时候会执行结束?就是当读到空白符空格,回车,换行,制表,翻页,垂直制表…)的时候才结束。

scanner.next 的行为是:尝试往后读,读到空白符就结束,会返回一个字符串,返回值里是不包含刚才最后的空白符的。由此可见,我们在客户端发送的信息中再加个空白符就行了。

此处会有人说可以用 nextline,nextline 是判定 /n,next 是判断空白符(包含 /n),都差不多。

所以此处最好的修改就是把 next 后面含有 write 的写法改成 println

客户端中第二步:把请求发送给服务器改为:printWriter.println(request);
服务器中第三步:把响应返回给客户端改为:printWriter.println(response);

改变后的代码执行就正确了

【网络编程】TCP_第5张图片
【网络编程】TCP_第6张图片

但是但是但是!!!bug 并没有全部解决,还有两个 bug。

  1. 上述代码中,使用 clientSocket !用完之后,是否要关闭呢?当然要关闭!和上面的 ServerSocket 不同!

生命周期不同,ServerSocket 的生命周期,是跟随整个程序的,clientSocket 的生命周期,只是当前连接!就应该在连接之后,把这里的 socket 进行关闭。如果不关闭,妥妥的资源泄露,ServerSocket 只有一个,clientSocket 会有无数个!每个客户端的连接,都是一个!!!

因此解决方案就是在 try 后面加个 finally ,finally 中写 clientSocket.close(); 即可

  1. 上述代码无法处理多个客户端

当第一个客户端连上服务器之后,服务器提示 “建立连接”
当第二个客户端也连接上的时候,服务器没提示
 
第一个客户端发的请求,可以正常处理
第二个客户端发的请求,不能处理
 
当把第一个客户端关闭之后,第二个客户端才能真正连接成功,之前发的请求,才有回应。

在服务器代码中:

【网络编程】TCP_第7张图片
【网络编程】TCP_第8张图片

当第一个客户端连接上之后,服务器就进入了 processConnect 代码,就在 while 循环 next 这里阻塞了,processConnect 方法就执行不完了,同时也就无法第二次执行到 accept ,也就无法处理第二个客户端。

next 和 循环 影响了服务器第二次调用 accept

因此解决方案就是既能够执行到 processConnect 里面的循环,又能够执行到 accept 即可,所以在这里,我们可以使用多线程。在主线程中执行 accept ,创建一个新的线程来调用 processConnect ,这样原来主线程就不会受影响。

【网络编程】TCP_第9张图片

执行效果:

  • 客户端1
    【网络编程】TCP_第10张图片

  • 客户端2
    【网络编程】TCP_第11张图片

  • 客户端3
    【网络编程】TCP_第12张图片

  • 服务器
    【网络编程】TCP_第13张图片

虽然比最开始单线程有提升,但是涉及到频繁创建销毁线程,在高并发的状态下,负担比较重,因此这里可以再优化使用线程池。

使用线程池,来解决频繁创建销毁线程的问题

//注意线程池在循环外面创建
ExecutorService service = Executors.newCachedThreadPool();
//循环内部
service.submit(new Runnable() {
    @Override
    public void run() {
        try {
            processConnect(clientSocket);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

当前是使用了线程池解决了问题,但是如果并发量太高了!就会导致池子里的线程特别特别多!就说明内存等资源调度开销特别大,,于是乎有了进一步的改进,是否能减少线程的数目?当前是一个线程对应一个客户端,能不能让一个线程对应多个客户端?当然可以,但是此处就不再拓展了,想知道的老铁可以去搜搜 “IO 多路复用”,本质上就是一个线程处理多个 socket ,操作系统提供的机制。

最后附上改进后的总代码:

服务器:

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("服务器启动!");
        ExecutorService service = Executors.newCachedThreadPool();
        while (true){
            //如果当前没有客户端来进行连接,就会阻塞等待
            Socket clientSocket = serverSocket.accept();
            //不再直接调用了,创建一个新的线程,让线程来调用他
            //版本1: 使用单线程,存在 bug ,无法处理多线程情况
            //processConnect(clientSocket);

			//版本2: 使用多线程,主线程负责拉客,新线程负责通信
			//虽然比版本 1 有提升,但是涉及到频繁创建销毁线程,在高并发的状态下,负担比较重
            //Thread t = new Thread(()->{
            //    try {
            //        processConnect(clientSocket);
            //    } catch (IOException e) {
            //        e.printStackTrace();
            //   }
            //});
            //t.start();

			//版本3: 使用线程池,来解决频繁创建销毁线程的问题
			//此处不太适合使用 "固定个数的"
			//在外面创建线程池
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnect(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    //通过这个方法,给当前连上的这个客户端,提供服务
    //一个连接过来了,服务方式可能有两种
    //1. 一个连接只进行一次数据交互(一个请求 + 一个响应)  短连接
    //2. 一个连接进行多次数据交互(N 个请求 + N 个响应)   长连接
    //此处来写长连接版本
    public void processConnect(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 建立连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);

            //这里是长连接的写法,需要用循环来获取到多次交互的情况
            while (true){
                if(!scanner.hasNext()){
                    //断开连接,当客户端断开连接的时候,此时 hasNext 就会返回 false
                    System.out.printf("[%s:%d] 断开连接!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //1. 读取请求并解析
                String request = scanner.next();
                //2. 根据请求计算响应
                String response = process(request);
                //3. 把响应返回给客户端
                printWriter.println(response);
                // 刷新一下缓冲区,避免数据没有发出去
                printWriter.flush();
                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(),
                        request,response);
            }
        }finally {
            //加在这里是更稳妥的做法
            clientSocket.close();
        }
    }
    public String process(String req){
        return req;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(8000);
        server.start();
    }
}

客户端:

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() throws IOException {
        //new 这个对象,是要和服务器建立连接的
        //建立连接,是要知道服务器在哪里
        socket = new Socket("127.0.0.1",8000);
    }

    public void start() throws IOException {
        //由于我们建立的是长连接,一个连接会处理 N 个请求和响应
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true){
                //从控制台读取用户输入
                System.out.println("> ");
                String request = scanner.next();
                //2. 把请求发送给服务器
                printWriter.println(request);
                printWriter.flush();
                //3. 从服务器读取响应
                String response = scannerNet.next();
                //4. 把结果显示到界面上
                System.out.printf("req : %s; resp : %s\n",request,response);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient();
        client.start();
    }
}

TCP 版本的字典查找

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class TcpDictServer extends TcpEchoServer{
    private Map<String,String> dict = new HashMap<>();

    public TcpDictServer(int port) throws IOException {
        super(port);

        dict.put("cat","小猫");
        dict.put("dog","小狗");
        dict.put("fuck","卧槽");
    }

    public String process(String req){
        return dict.getOrDefault(req,"俺也不知道是啥");
    }

    public static void main(String[] args) throws IOException {
        TcpDictServer server = new TcpDictServer(8000);
        server.start();
    }
}

运行结果展示:

【网络编程】TCP_第14张图片
【网络编程】TCP_第15张图片

【网络编程】TCP_第16张图片

你可能感兴趣的:(Java,EE初阶,网络,tcp/ip,服务器)