网络编程原理一

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 网络编程原理一

一、Socket 套接字



引言


操作系统提供的网络编程 API 叫做 socket API,socket API 中涉及到一个核心概念 socket


而 socket 本质上是一个文件描述符。我们通过 socket 文件来操作网卡。


我们都知道,对于计算机来说,键盘是一个标准输入文件;显示器是一个标准输出文件。因为操作系统在管理硬件设备和软件资源的时候,为了风格统一,就都通过文件来管理。所以一切皆文件,而网卡也是一个硬件设备,操作系统也是用文件来管理网卡的,所以此处用来表示网卡设备的文件,也就是 socket 文件。


两类 socket


操作系统提供的 socket API 主要有两类:TCP / UDP


TCP:Transmission Control Protocol ( 传输控制协议 )
UDP:User Datagram Protocol ( 用户数据报协议 )


1. 流套接字 ( 底层使用了 TCP 协议 )
特点:
有连接
可靠传输
面向字节流
全双工
2. 数据报套接字 (底层使用了 UDP 协议)
特点:
无连接
不可靠传输
面向数据报
全双工


TCP 和 UDP 两者的区别与联系



1. 连接的区别


有连接相当于打电话。无连接就相当于发微信。


举个例子:A 给 B 打电话,首先会有一个等待过程,就是 【嘟嘟嘟…】这个尝试建立连接的过程,注意,此时还只是尝试连接!只有当 B 按下了手机的接听键,说明他们之间真正地连接了,也就能真正地进行通信了!


而如果 A 给 B 发微信,B 不需要像接电话那样尝试建立连接,只要 B 有网络,就能够接收数据。然而,B 接收到了微信消息,但没有打开微信看信息,或者说,看了 A 发来的信息,之后没有回复他。那么就是另外一回事了…


2. 传输的区别


可靠传输:发送方能够知道接收方是不是收到了数据。

不可靠传输:发送方不知道接收方是不是收到了数据。


注意:可靠性传输并不代表发送的数据能 100% 让接收方获取到。此外,可靠性传输并不代表安全性传输,安全性传输是指:A 在给 B 传输数据的过程中,不会被第三方 C 截获数据。或者说,就算传输过程中被 C 截获到了,C 也不知道数据是啥。


3. 字节流与数据报的区别


面向字节流:假设 A 准备给 B 发送 100 个字节的数据,A 可以一次发 1个 字节,重复100次,也可以一次发 10个 字节,重复 10次。


上述的解释完美地体现了以面向字节流发送数据的灵活性,接收数据其实也是一样的。


面向数据报:以一个个的数据报为基本单位 ( 每个数据报多大,不同的协议里面是有不同的约定 )

发送的时候,一次至少发一个数据报 ( 如果尝试发送一个半,实际只能发出去一个 )

接收的时候,一次至少收一个数据报 ( 如果尝试接收半个,剩下半个就没了 )


4. 全双工


TCP 和 UDP 都是基于全双工的方式,那么我们就来讨论一下全双工和半双工的区别。


全双工:双向通信,A 和 B 可以同时向对方发送接收数据。

半双工:单向通信,要么 A 给 B 发,要么是 B 给 A 发,不能同时发。


可以将传输数据想象成用水灌入水管,如下图所示:


f553c22ee73f4d968b42223045b1eb10.png


二、客户端和服务器之间的通信流程 (重点)



客户端和服务器之间的通信流程如下,顺序按照时间轴和序号。

在下图中,最复杂的步骤是第三步,即服务器根据客户端的请求来计算响应。


a76c9975934d4eeaa2b68a2976af0829.png


其实客户端也需要和用户进行交互,下面的程序大都是通过 Scanner 类 这个标准输入来让用户输入具体的需求,即命令行的方式实现交互。


这就好比:你现在正在使用浏览器,然而浏览器需要知道用户到底是想查资料、还是看视频、还是打游戏…你得把你的需求 / 命令告诉他。


三、UDP数据报套接字编程流程



引言


UDP socket 中有三个核心的类:


1. DatagramSocket 描述一个 socket 对象
2. DatagramPacket 描述一个 UDP 数据报
3. InetSocketAddress 描述客户端/服务器的 ip 和 port


1. 基于 UDP 的 socket 来写一个简单的回显程序


回显程序:Echo


回显程序说白了就是:A 给 B 说什么,B 就给 A 回应什么,相当于是复读机。


创建包 demo1,用 [ UdpEchoClient 类 ] 描述客户端,用 [ UdpEchoServer 类 ] 描述服务器


6f4036ac81ef4363adb4faddb3e5484c.png


(1) UdpEchoServer 类


package demo1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
    private DatagramSocket socket = null;
    //port 表示端口号
    //服务器在启动的时候,需要关联(绑定)上一个端口号
    //收到数据的时候,就会根据这个端口号来决定把数据交给哪个进程
    //虽然此处 port 写的类型是 int, 但实际上端口号是一个 两字节的无符号整数
    //范围 0 ~ 65535
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }
    /**
     * 通过 start 方法来启动服务器
     */
    public void start() throws IOException {
        System.out.println("服务器启动!");
        //服务器一般都是持续运行的
        while (true) {
            //2. 服务器读取客户端发来的请求并解析
            //   读取请求,当前服务器不知道客户端什么时候发来请求,所以 receive 方法就会发生阻塞等待
            //   如果真的有请求过来了,此时 receive 方法就会返回
            //   参数 DatagramPacket 是一个输出型参数,在 socket 中读到的数据会设置到这个参数的对象中
            //   DatagramPacket 在构造的时候,需要指定一个缓冲区(缓冲区其实就是一段内存空间,通常使用 byte[])
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            //把 requestPacket 对象中的内容取出来,作为一个字符串
            //即从 requestPacket 得到的数据,从 0 到最终的长度放入到 String 中去
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //3. 根据请求计算响应
            String response = process(request);
            //4. 构造响应并返回
            //   把响应写回到客户端,这时候也需要构造以一个 DatagramPacket
            //   此处给 DatagramPacket 中设置长度,必须是 "字节的个数"
            //   如果直接读取 response.length,是错误的!因为它表示[字符串的长度],也就是[字符的个数],那么这是错误的!
            //   当前的 responsePacket 在构造的时候,还需要指定这个包发给谁
            //   其实发送的目标,就是发请求的那一方
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                                                                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //   加上打印日志
            //   format 即格式化字符串的方式,类似于 C 语言中的 sprintf
            //   和 C语言一样,%d 表示要打印一个有符号十进制的整数,%s 表示要打印一个字符串
            String log = String.format("[%s, %d] req: %s; resp: %s",
                    requestPacket.getAddress().toString(),
                    requestPacket.getPort(),
                    request, response);
            System.out.println(log);
        }
    }
    /**
     * process 方法根据请求来计算响应
     * 由于当前是一个回显服务器,就把客户端发送的请求直接返回即可
     */
    private String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        //new 一个服务器对象
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}



(2) UdpEchoClient 类


package demo1;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        //客户端在发送请求之前,需要知道 服务器的 IP 地址 和 端口号
        //因为只有商家知道了收件人的地址和号码,才能将商品发出去!
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        //客户端这里不能指定端口号,尤其是不能将 serverPort 放进去
        //这里没有指定端口,就相当于操作系统会自动分配一个空闲的端口号,给客户端使用
        this.socket = new DatagramSocket();
    }
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            //从标准输入读入一个数据
            System.out.print("->");
            String request = scanner.nextLine();
            if (request.equals("exit")) {
                System.out.println("exit!");
                return;
            }
            //1. 客户端构造请求并往服务器中发送
            //   把字符串构造成一个 UDP 请求,并发送数据
            //   在这个 DatagramPacket 中,既要包含即将发送的具体数据,又要指定这个数据即将发给哪个服务器
            //   下面在传入服务器 IP 的时候,要通过 getByName方法转换一下,而传入服务器的端口号时,正常写入即可
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket); //send 方法就开始发送了!
            //5. 客户端从服务器中获取响应
            //   这里 receive 方法接收数据的时候,将数据放入 responsePacket 参数中去。
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            //6. 显示响应结果
            String log = String.format("req: %s; resp: %s", request, response);
            System.out.println(log);
        }
    }
    public static void main(String[] args) throws IOException {
        //new 一个客户端对象
        // [ 127.0.0.1 ] 这是一个环回 IP,其实就表示主机本身
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}


通信结果:


589e68307ff94293a99ee6515ef801ab.png


(3) 注意几个点


① 客户端是主动的一方,服务器是被动的一方。


举个例子:客户端,顾名思义就是客户,服务器顾名思义就是服务的人。我们将客户端想成汽车,服务器想成加油站,加油站有很多,汽车也有很多,但毋庸置疑,汽车的数量一定远远超过加油站的数量。所以,当汽车没有油了,驾驶员就需要选择去哪个加油站去加油;加油站就要等着客户到来,客户没来,就干等着,客户来了,就忙起来。那么,汽车就是主动的一方,加油站就是被动的一方。


② 客户端可以不指定端口号,尤其是不能将服务器的端口号当成某个客户端的端口。

举个例子:如果说 IP 地址是收件人的地址,那么端口号就是收件人的电话号码。所以说,端口不能乱填。


而当我们没有为客户端指定端口的时候,操作系统会自动分配一个空闲的端口号,给客户端使用。那么为什么客户端可以自动生成,而服务器就必须手动指定呢?


因为 客户端是主动发起请求的一方,服务器是被动接受请求的一方。


举个例子:客户端是商家,服务器是收件人,只有收件人把地址和手机号码告诉了商家,商家才能够根据收件人填写的信息发货;当商家知道了收件人填写的地址和号码之后,商家就可以寄出商品了。


2e3eba98cdf6456a9b9cd46b396d0b61.png


③ 这里有一个 receive 方法,这个方法与我们平时的函数用起来不一样。以客户端的 receive 方法为例:


DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);


在上面的代码中,receive 得到的结果并没有作为一个返回值,而是作为一个输出型参数。

这种操作在 Java 中是相对少见的。Java中大部分情况下都是用返回值表示输出,用参数表示输入,很少会用参数表示输出。


什么意思呢?


上面的 receive 方法中,它在接收数据后,将数据写入到 responsePacket 这个参数去,而 responsePacket 却是作为参数传入 receive 方法中的。这和我们平时用参数传入函数,再利用函数返回这个思想完全相反。

实在不行,我们记住 receive 的这种格式即可,因为它真的很少见!


之所以这里要这么做,主要就是为了能够让用户给 DatagramPacket 指定一个缓冲区,那么为什么非得要用户来指定这个缓冲区?


因为缓冲区大小其实不好确定,如果是 DatagramPacket 自己随机指定大小,短了或长了都不合适,所以这是需要根据程序员的需求设定的!


此外,receive 是用来接收数据的,当数据没有过来的时候,receive 就会阻塞等待。


2. 基于 UDP 的 socket 写一个翻译程序


我们来写一个翻译程序:和查字典一样,英译汉。

在客户端的命令行输入 你好,服务器返回 hello.


创建包 demo1, 用 [ UdpDictClient ] 描述客户端,用 [ UdpDictServer ] 描述服务器。


8ad4bcde682742d9b3e41e303676cca4.png


(1) UdpDictServer


package demo1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDictServer {
    private DatagramSocket socket = null;
    public UdpDictServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }
    /**
     * 通过 process 方法来完成具体的查字典逻辑
     * 例如:输入 hello,返回 [你好]
     */
    private String process(String request) {
        //像有道词典这种专业词典,也是通过查表的逻辑来实现的,但也许是将待查的数据全放在了数据库中
        Map<String, String > map = new HashMap<>();
        map.put("hello", "你好");
        map.put("cat", "猫咪");
        map.put("computer", "计算机");
        return map.getOrDefault(request, "抱歉,你所查询的英文在字典中不存在");
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            //2. 读取请求并解析请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //3. 根据请求计算响应
            String response = process(request);
            //4. 构造响应并返回响应
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                    response.getBytes().length, requestPacket.getSocketAddress() );
            socket.send(responsePacket);
            //顺便打印日志
            String log = String.format("[%s, %d] req: %s, resp: %s ",
                    requestPacket.getAddress().toString(), requestPacket.getPort(),
                    request, response);
            System.out.println(log);
        }
    }
    public static void main(String[] args) throws IOException {
        UdpDictServer server = new UdpDictServer(9090);
        server.start();
    }
}


(2) UdpDIctClient


package demo1;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpDIctClient {
    private String serverIp;
    private int serverPort;
    private DatagramSocket socket = null;
    public UdpDIctClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();
    }
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("输入需要翻译的英文 -> ");
            String request = scanner.nextLine();
            if (request.equals("exit")) {
                System.out.println("exit!");
                return;
            }
            //1. 构造请求并发送请求
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                    request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket);
            //5. 尝试获取响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            //6. 显示响应结果
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            String result = String.format("req: %s, resp: %s ", request, response);
            System.out.println(result);
        }
    }
    public static void main(String[] args) throws IOException {
        UdpDIctClient client = new UdpDIctClient("127.0.0.1", 9090);
        client.start();
    }
}


通信结果:


af406f065d774e33a8e3ef2e489fb51b.png

(3) 注意几个点


① 通过回显程序和翻译程序之间的对比,我们可以看到,里面很多代码的写法是固定的。


比如:客户端发送请求和接收响应,服务器读取请求和返回响应,构造 socket 对象…而服务器计算响应、用户和客户端之间的交互有所不同。


② 回显程序和翻译程序的主要代码差异在于服务器计算响应这一步。


上面提到,最复杂、最核心其实就是服务器根据请求来计算响应那一步,因为这不仅需要适应场景来采取不同的策略,还需要根据用户和客户端之间的交互来进行设计。


四、TCP字节流套接字编程流程



TCP socket 中有五个核心的类:


1. ServerSocket  描述一个服务器
2. Socket  描述一个客户端
3. InputStream  读取文件
4. OutputStream  写入文件
5. InetAddress  描述客户端/服务器的 ip 和 port


服务器:


  • 创建 ServerSocket 关联上一个端口号,称为 listenSocket
  • 调用 ServerSocket 的 accept 方法,accept 的功能是把一个内核建立好的连接给拿到代码中处理,accept 会返回一个 Socket 实例,称为 clientSocket
  • 使用 clientSocket 的 getInputStream 和 getOutputStream 方法得到字节流对象,就可以进行读取和写入了。
  • 当客户端断开连接之后,服务器就应该要及时关闭 clientSocket ( 否则可能会出现文件资源泄露的情况 )


客户端:


  • 创建一个 Socket 对象,创建的同时指定服务器的 ip 和 端口,这个操作就会让客户端和服务器建立 TCP 连接。在这个连接建立的过程,就是传说中的 " 三次握手 ",这个流程是内核完成的,用户代码感知不到。


  • 接着,客户端就可以通过 Socket 对象的 getInputStream 和 getOutputStream 来和服务器进行通信了。


1. 基于 TCP 的 socket 来写一个简单的回显程序


(1) TcpEchoServer


package demo1;
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 listenSocket = null;
    public TcpEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            //先尝试建立连接,建立好连接前,accept 方法会阻塞等待,
            //直到客户端建立连接了,此时 accept 会返回一个 Socket 对象
            //接下来服务器与客户端之间的通信就通过 clientSocket 来完成
            Socket clientSocket = listenSocket.accept();
            processConnection(clientSocket);
        }
    }
    private void processConnection(Socket clientSocket) throws IOException {
        String log = String.format( "[%s : %d] 客户端上线!",
                clientSocket.getInetAddress().toString(), clientSocket.getPort() );
        System.out.println(log);
        try( InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream() ) {
            while (true) {
                //2. 读取请求并解析
                Scanner scanner = new Scanner(inputStream, "utf-8");
                if (!scanner.hasNext()) {
                    log = String.format( "[%s : %d] 客户端下线!",
                            clientSocket.getInetAddress().toString(), clientSocket.getPort() );
                    System.out.println(log);
                    //使用 break 而不使用 return 的原因,是为了让代码走到 finally 语句的 close 方法
                    break;
                }
                //一开始阻塞等待,直到客户端往服务器发送数据
                String request = scanner.nextLine();
                //3. 根据请求并计算响应
                String response = process(request);
                //4. 构造响应并返回
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();
                log = String.format("[%s : %d] req: %s; resp: %s",
                        clientSocket.getInetAddress().toString(), clientSocket.getPort(),
                        request, response);
                System.out.println(log);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            //连接结束,关闭资源
            clientSocket.close();
        }
    }
    private String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}


(2) TcpEchoClient


package demo1;
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 String serverIp;
    private int serverPort;
    private Socket socket = null;
    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        this.socket = new Socket(serverIp, serverPort);
    }
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                System.out.println("输入 ->");
                String request = scanner.nextLine();
                if (request.equals("exit")) {
                    System.out.println("Exit!");
                    //使用 break 而不使用 return 的原因,是为了让代码走到 finally 语句的 close 方法
                    break;
                }
                //1. 构造请求并发送
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                printWriter.flush();
                //5. 尝试获取响应
                Scanner respScanner = new Scanner(inputStream, "utf-8");
                String response = respScanner.nextLine();
                //6. 显示响应结果
                String result = String.format("req: %s; resp: %s", request, response);
                System.out.println(result);
            }
        }catch (IOException e) {
            e.printStackTrace();
        } finally {
            socket.close();
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}


(3) 注意几个点


① TCP 需要先建立连接


UDP 的服务器进入主循坏,就直接尝试 receive 读取请求了。但 TCP 是有连接的,先需要做的是,建立好连接。当服务器运行的时候,当前是否有客户端来建立连接,不确定。如果客户端没有建立连接,accept 方法就会阻塞等待,直到有客户端建立连接了,此时 accept 才会返回一个 Socket 对象,进一步的服务器和客户端之间的交互,就交给 clientSocket 来完成了。


举个例子:我们将这个连接机制比作成 A 给 B 打电话,A 开始拨号,那么就会有【嘟嘟嘟…】的过程,这个过程就是尝试在建立连接,如果 B 接电话了,说明就连接上了,A 与 B 之间就能够通信。反之,如果受电线的影响、号码错误、或者说 B 故意不接电话,这就是连接失败…而当 A 与 B 通话后,B 挂断了电话,说明退出连接了。那么在这个例子中,listenSocket 就用来描述这个拨号过程,这个过程 A 尝试与 B 进行建立预备通信,如果 B 迟迟不接电话,或者因为其他物理原因,那么此时 A 就要进行阻塞等待;直到 B 接通电话,那么 clientSocket 就开始他们后续真正的通信过程。


② TCP 是通过字节流来发送和传输数据的


既然 TCP 与字节流密切相关,那么就不得不使用 InputStream 和 OutputStream 以及他们两之间对应的一些方法。此外,因为与输入输出有关,那么这里也应该注意在程序的末尾使用 close 方法,以防资源泄露的问题。我们可以使用 finally 语句,这样就避免了无论是程序正常运行,还是程序抛出了异常,都会执行 close 方法。


(4) 说明当前服务器的缺点


在上面的服务器代码中,有一个缺点:服务器与客户端之间实际上只能一对一的进行通信。


dbc99b6c23f448d5b8758cf518f765e9.png

我们之前将服务器比作加油站,客户端比作没油的汽车,那么我们都知道,汽车的数量是远远大于加油站的。而我们所要做的就是让多辆汽车能够同时加油,所以服务器与客户端也是如此。服务器需要为更多的客户端服务,并且需要同时服务,因为如果做不到同时服务,服务器大部分时间或许就是在阻塞等待中,这就会大大降低效率。


2. 改进服务器


如果要保持两个及多个客户端同时在一台服务器上运行,那么就需要两个及多个线程来跑。


解决方案:主线程中循环调用 accept 方法,每次获取到一个新的客户端连接,就创建一个线程,让这个线程实现服务器为当前的客户端服务。


(1) TcpThreadEchoServer


package demo2;
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 listenSocket  = null;
    public TcpThreadEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            //在这个代码中,通过创建线程,就能保证在 accept 被调用完后,立刻有线程再次调用 accept
            Socket clientSocket = listenSocket.accept();
            //创建一个线程来为这个客户端提供服务
            Thread t = new Thread() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            };
            t.start();
        }
    }
    private void processConnection(Socket clientSocket) throws IOException {
        String log = String.format("[%s : %d] 客户端上线!",
                clientSocket.getInetAddress().toString(), clientSocket.getPort());
        System.out.println(log);
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream() ){
            while (true) {
                //2. 读取请求并解析
                Scanner scanner = new Scanner(inputStream, "utf-8");
                if (!scanner.hasNext()) {
                    log = String.format("[%s : %d] 客户端下线!",
                            clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    System.out.println(log);
                    break;
                }
                String request = scanner.nextLine();
                //3. 根据请求并计算响应
                String response = process(request);
                //4. 构造响应并返回响应
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();
                log = String.format("[%s : %d], req: %s; resp: %s",
                        clientSocket.getInetAddress().toString(), clientSocket.getPort(),
                        request, response);
                System.out.println(log);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            clientSocket.close();
        }
    }
    private String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        TcpThreadEchoServer tcpThreadEchoServer = new TcpThreadEchoServer(9090);
        tcpThreadEchoServer.start();
    }
}


(2) 使用线程池的方式


普通版本 ( 不能处理多个客户端 )

多线程版本 ( 能处理多个客户端,但需要频繁创建销毁线程 )

线程池版本 ( 能处理多个客户端,也不需要频繁创建销毁线程 )


package demo3;
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 listenSocket = null;
    public TcpThreadPoolEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService service = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = listenSocket.accept();
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    private void processConnection(Socket clientSocket) throws IOException {
        String log = String.format("[%s : %d] 客户端上线!",
                clientSocket.getInetAddress().toString(), clientSocket.getPort());
        System.out.println(log);
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            while (true) {
                //2. 读取请求并解析
                Scanner scanner = new Scanner(inputStream, "utf-8");
                if (!scanner.hasNext()) {
                    log = String.format("[%s : %d] 客户端下线!",
                            clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    System.out.println(log);
                    break;
                }
                String request = scanner.nextLine();
                //3. 根据请求并计算响应
                String response = process(request);
                //4. 构造响应并返回响应
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();
                log = String.format("[%s : %d], req: %s; resp: %s",
                        clientSocket.getInetAddress().toString(), clientSocket.getPort(),
                        request, response);
                System.out.println(log);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }
    }
        private String process(String request) {
            return request;
        }
    public static void main(String[] args) throws IOException {
        TcpThreadPoolEchoServer server = new TcpThreadPoolEchoServer(9090);
        server.start();
    }
}


3. 基于 TCP 的 socket 写一个翻译程序


由于服务器针对请求计算响应这一步不同,所以我们使用 TcpDictServer 继承TcpThreadPoolEchoServer ,接着重写 process 方法即可。


TcpDictServer


package demo4;
import demo3.TcpThreadPoolEchoServer;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class TcpDictServer extends TcpThreadPoolEchoServer {
    public TcpDictServer(int port) throws IOException {
        super(port);
    }
    //start 方法不变,processConnection 方法不变
    //process 方法需要改变
    @Override
    public String process(String request) {
        Map<String, String> map = new HashMap<>();
        map.put("hello", "你好");
        map.put("cat", "猫咪");
        map.put("computer", "计算机");
        return map.getOrDefault(request, "你所查询的英文在字典中不存在");
    }
    public static void main(String[] args) throws IOException {
        TcpDictServer tcpDictServer = new TcpDictServer(9090);
        tcpDictServer.start();
    }
}


注意


假设极端情况下,一个服务器面临很多很多客户端,这些客户端在连上了之后,并没有退出,也就是说:这些客户端正在运行中,那么这个时候服务器这边可能同一时刻,就会存在很多很多线程。比方说,大型的网络游戏,一个区服可能就有上万个线程,甚至更多,因为玩家需要联机作战。这就能解释,为什么双11 期间,或者省级、国家级考试分数下来,学生通过网页在查询数据的时候,迟迟进不去一些网页,甚至网页又是直接崩溃,之后在你的屏幕上显示一串报错代码!


解决上面高并发问题的方案:


① 使用协程来代替线程,完成并发,因为协程的比线程更轻量,( GO 语言 )


② 可以使用 IO 多路复用的机制,完成并发。此方法是从根本上解决服务器处理高并发的问题

在内核里来支持这样的一个功能。


对于网络编程,假设有 5000 个客户端,在服务器这边会用一定的数据结构来把这 5000 个客户端对应的 socket 都存好,不需要一个线程对应一 个客户端,我们只创建几个线程,IO 多路复用机制,就能够做到当某个 socket 上有数据了,就通知到应用程序,让线程来从当前的 socket 中读数据。


③ 使用多个主机,或者说提供更多的硬件资源,这是釜底抽薪的办法。可以简单地理解为:《有钱是万能的,有钱就可以任性…》

目录
相关文章
|
9天前
|
机器学习/深度学习 人工智能 自然语言处理
深度学习的奥秘:探索神经网络的核心原理
本文将深入浅出地介绍深度学习的基本概念,包括神经网络的结构、工作原理以及训练过程。我们将从最初的感知机模型出发,逐步深入到现代复杂的深度网络架构,并探讨如何通过反向传播算法优化网络权重。文章旨在为初学者提供一个清晰的深度学习入门指南,同时为有经验的研究者回顾和巩固基础知识。
33 11
|
1月前
|
机器学习/深度学习 存储 算法
回声状态网络(Echo State Networks,ESN)详细原理讲解及Python代码实现
本文详细介绍了回声状态网络(Echo State Networks, ESN)的基本概念、优点、缺点、储层计算范式,并提供了ESN的Python代码实现,包括不考虑和考虑超参数的两种ESN实现方式,以及使用ESN进行时间序列预测的示例。
61 4
回声状态网络(Echo State Networks,ESN)详细原理讲解及Python代码实现
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
深度学习中的自适应神经网络:原理与应用
【8月更文挑战第14天】在深度学习领域,自适应神经网络作为一种新兴技术,正逐渐改变我们处理数据和解决问题的方式。这种网络通过动态调整其结构和参数来适应输入数据的分布和特征,从而在无需人工干预的情况下实现最优性能。本文将深入探讨自适应神经网络的工作原理、关键技术及其在多个领域的实际应用,旨在为读者提供一个全面的视角,理解这一技术如何推动深度学习向更高效、更智能的方向发展。
|
8天前
|
机器学习/深度学习 人工智能 自然语言处理
深度剖析深度神经网络(DNN):原理、实现与应用
本文详细介绍了深度神经网络(DNN)的基本原理、核心算法及其具体操作步骤。DNN作为一种重要的人工智能工具,通过多层次的特征学习和权重调节,实现了复杂任务的高效解决。文章通过理论讲解与代码演示相结合的方式,帮助读者理解DNN的工作机制及实际应用。
|
4天前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【9月更文挑战第14天】网络协议是机器间交流的约定格式,确保信息准确传达。主要模型有OSI七层与TCP/IP模型,通过分层简化复杂网络环境。IP地址全局定位设备,MAC地址则在本地网络中定位。网络分层后,数据包层层封装,经由不同层次协议处理,最终通过Socket系统调用在应用层解析和响应。
|
5天前
|
网络协议 网络架构 数据格式
TCP/IP基础:工作原理、协议栈与网络层
TCP/IP(传输控制协议/互联网协议)是互联网通信的基础协议,支持数据传输和网络连接。本文详细阐述了其工作原理、协议栈构成及网络层功能。TCP/IP采用客户端/服务器模型,通过四个层次——应用层、传输层、网络层和数据链路层,确保数据可靠传输。网络层负责IP寻址、路由选择、分片重组及数据包传输,是TCP/IP的核心部分。理解TCP/IP有助于深入掌握互联网底层机制。
23 2
|
30天前
|
缓存 网络协议 算法
网络编程原理
网络编程原理
|
30天前
|
网络协议 算法 安全
网络原理问题
网络原理问题
|
1月前
|
机器学习/深度学习 人工智能 算法
深度学习的奥秘:探索神经网络的核心原理
深度学习,一个听起来既神秘又充满魔力的词汇,它如同一扇通往未知世界的大门,背后隐藏着无尽的智慧与可能。本文将以一种通俗易懂的方式,带领读者走进深度学习的世界,探索那些构成神经网络核心的基本原理。我们将从最初的感知机模型出发,逐步深入到复杂的多层网络结构,揭示数据如何在这些网络中流动、变化,最终实现智能决策的过程。通过这篇文章,你将了解到深度学习不仅仅是技术的堆砌,更是对自然界智慧的一种模仿与致敬。
45 1
|
17天前
|
存储 监控 安全