网络编程——JavaEE

网络编程套接字

网络编程套接字:是操作系统给应用程序提供的一组 API (socket API)。socket API 可以视为是 应用层 和 传输层 之间的通信桥梁。传输层的核心协议有两种,TCP UDP,socket API 也有对应的两组。由于 TCP 和 UDP 协议,差别很大,所以这两组 API 差别也挺大

  1. TCP:有连接,可靠传输,面向字节流,全双工。
  2. UDP:无连接,不可靠传输,面向数据报,全双工。
  3. 有链接:相当于打电话,得先接通,才能交互数据。
  4. 无连接:像发微信,不需要接通,直接就能发数据。
  5. 可靠传输:传输过程中,发送方知道接收方有没有收到数据。打电话就是可靠传输。已读也是可靠传输。
  6. 不可靠传输:传输过程中,发送方不知道接收方有没有收到数据。发微信就是不可靠传输。
  7. 面向字节流:以字节为单位进行传输(非常类似于 文件操作 中的字节流)。
  8. 面向数据报:以数据报为单位进行传输(一个数据报都会明确大小)。一次发生/接收必须是一个完整的数据报,不能是半个,也不能是一个半。
  9. 全双工:一条链路双向通信。
  10. 半双工:一条链路单向通信。

UDP

UDP socket 比 TCP 更简单,主要涉及到两个类:

  1. DatagramSocket,这个 DatagramSocket 对象对应到操作系统当中的一个 socket 文件。平时说的文件,只是指普通文件(硬盘上的数据)。实际上,操作系统中的文件还可能表示了一些硬件设备/软件资源。socket 文件,就对应着 ”网卡“ 这种硬件设备,从 socket 文件读数据,本质上就是读网卡。往 socket 文件写数据,本质上就是写网卡。可以认为 socket 文件就是一个遥控器,通过遥控器来操作网卡。核心方法:receive 接收数据 send 发送数据 close 释放资源。
  2. DatagramPacket,代表了一个 UDP 数据报,使用 UDP 传输数据的基本单位。每次发送/接收数据,都是在传输一个 DatagramPacket 对象。

回显服务

回想服务就是请求内容是啥,响应就是啥。我们把整个回显服务分为两部分:

  1. UdpEchoServer 服务器部分。
  2. UdpEchoClient 客户端部分。

服务器部分

  1. 网络编程的时候,先准备好 socket 实例。socket 实例是进行网络编程的大前提。构造 socket 对象失败的原因:
    a、端口号已经被占用。
    b、每个进程能够打开的文件数是有上限的,如果进程之前就已经打开了很多很多的文件,就可能导致此处的 socket 文件就不能顺利打开了。
private DatagramSocket socket = null;
  1. 绑定端口号,可以在运行程序的时候来指定。
public UdpEchoServer(int port) throws SocketException {
    socket = new DatagramSocket(port);
}
  1. 多个进程不能绑定同一个端口。一个进程可以绑定多个端口,就是创建多个 socket 对象,然后绑定多个端口。

启动服务器:

  1. 因为 UDP 不需要建立连接,所以直接接收从客户端发来的数据即可。通过 DatagramPacket 来构造一个对象,然后通过 socket.receive() 来填充数据就好。receive 可能会引起阻塞。
  2. 根据请求计算相应。因为这里是回显服务,所以直接返回数据就可以了。
  3. 把响应写回到客户端。通过 sent 方法就可以写回。返回的时候再构造一个 DatagramPacket 对象返回就好。
  4. 在发送数据的时候,必须指定这个数据要发给谁,就是指定 IP 和 端口。
  5. 一个服务器 可以有很多个客户端。产生多少客户端,要通过性能测试来看。

服务器代码如下:

public class UdpEchoServer {

    //准备好 socket 实例。
    private DatagramSocket socket = null;
    //绑定端口号。
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }
    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true) {
            //1、读取客户发来的请求
            //为了接收数据,需要准备好一个空的 DatagramPacket 对象。
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
            //2、根据请求计算响应(由于这是一个回显服务,所以这一步就可以省略)
            String response = process(request);
            //3、把响应写回到客户端。
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            // 在当前的情景当中,就是谁发来的请求,就再返还给谁。
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req: %s, resp: %s\n",
                    requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
        }
    }
    //由于是回显服务,响应就和请求一样了
    public String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

客户端部分

在客户端的端口号,就不用自己手动指定了。让操作系统自己分配一个空闲的端口号就可以了。

  1. 客户端也要构造 UDP 请求。
  2. 客户端要有 IP。
  3. 客户端要有端口号。

客户端的主要作用:

  1. 从控制台读取用户输入的字符串。

  2. 把用户输入的内容,构造成一个 UDP 请求,并发送。构造的信息包含两部分信息:
    a、数据的内容 request 字符串。
    b、数据要发给谁 服务器的 IP + 端口。

  3. 从服务器响应数据,并解析。

  4. 把响应结果显示到控制台上。

客户端代码:

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;
    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        //此处的 port 是服务器的端口
        //客户端启动的的时候,不需要给 socket 指定端口,客户端自己的端口是系统自己分配的
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }
    public UdpEchoClient() throws SocketException {
        socket = new DatagramSocket();
    }
    public void start() throws IOException {
        System.out.println("启动客户端");
        Scanner scanner = new Scanner(System.in);
        while (true) {
            //1、从控制台读取用户输入的字符串
            String request = scanner.next();
            //2、把用户输入的内容,构造成一个 UDP 请求并发送
            //构造的信息包含两部分信息:1、数据的内容 request 字符串    2、数据要发给谁 服务器的 IP + 端口
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIP),serverPort);
            socket.send(requestPacket);
            //3、从服务器响应数据,并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
            //4、把响应结果显示到控制台上
            System.out.printf("req: %s, resp: %s\n", request,response);
        }
    }
    public static void main(String[] args) throws IOException {
        //因为客户端和服务器在同一个端口上,使用的 Ip 仍然是 127.0.0.1 如果在不同的机器上面,就要修改 IP 了
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

效果展示

先启动服务器:
在这里插入图片描述
再启动客户端:
在这里插入图片描述
测试效果:
在这里插入图片描述
在这里插入图片描述

加一些业务逻辑

通过在回显服务的基础上,加一些业务逻辑,就可以模拟实际开发当中的使用。 这里用一个翻译字典来展示:

  1. 客户端代码不变。
  2. 主要调整 process 方法。
  3. 读取请求并解析,把响应写回给客户端,这俩步骤都一样。

关键的就是:根据请求处理响应。只需要重写 process 方法就可以了,所以通过继承来写。

通过哈希表来构造一个词典。

代码如下:

public class TranslateServer extends UdpEchoServer{
    private HashMap<String, String> translate = new HashMap<>();
    public TranslateServer(int port) throws SocketException {
        super(port);
        translate.put("cat","小猫");
        translate.put("dog","小狗");
        translate.put("pig","小猪");
    }
    @Override
    public String process(String request) {
        return translate.getOrDefault(request,"没有该词的翻译");
    }
    public static void main(String[] args) throws IOException {
        TranslateServer server = new TranslateServer(9090);
        server.start();
    }
}

运行效果:
在这里插入图片描述

TCP

TCP 是以字节流为单位的。TCP 的 API 当中主要涉及到两个类:

  1. ServerSocket API(专门给 TCP 服务器使用)
  2. Socket API(既要给服务器用,又要给客户端用)

回显服务

服务器部分

建立连接

因为 TCP 是有链接的,不能一上来就读数据,而要先建立连接(接电话)。通过 Socket 里面的 accept 来建立链接:

public void start() throws IOException {
    System.out.println("服务器启动");
    while (true) {
    	//“接电话” 建立连接。
        Socket clientSocket = serverSocket.accept();
        processConnection(clientSocket);
    }
}
处理请求

处理请求的时候,先获得 IP 和端口号。 针对 TCP socket 的读写就和文件读写是一模一样的。
处理请求代码部分:

private void processConnection(Socket clientSocket) {
    System.out.printf("[%s:%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
    //处理请求和响应
    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] 客户端断开连接",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                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();
        }
    }
}
服务器端代码
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();
            processConnection(clientSocket);
        }
    }
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //处理请求和响应
        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] 客户端断开连接",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    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 {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

客户端部分

也用普通的 socket 即可。不用手动给客户端绑定端口,系统会自动分配。

  1. 对于 TCP 的 ServerSocket 来说,构造方法指定的端口,也表示自己绑定哪个端口。
  2. 对于 TCP 的 Socket 来说,构造方法指定的端口。表示要链接的服务器的端口。

流程和 UDP 一样,不过因为是字节流,所以是用文件来操作的。代码如下:

public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String serverIP, int serverPort) throws IOException {
    	//获得 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、从控制台读取字符串
                    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("res: %s, resp: %s\n",request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

效果展示

客户端:
在这里插入图片描述
服务器端:
在这里插入图片描述

多客户端回显服务多线程版本

因为上面的版本,一次只能接通一个会话,也就是一次只能连接一个客户端,所以实现一个多客户端的。通过多线程就可以实现多客户端了。其他地方都不要修改。只需要修改接通链接的地方就好了。修改后的代码如下:

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) {
            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());
        //处理请求和响应
        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] 客户端断开连接",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    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 {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

运行结果:

我们创建三个客户端来测试:

客户端1:
在这里插入图片描述
客户端2:
在这里插入图片描述
客户端3:
在这里插入图片描述
服务器端:
在这里插入图片描述
通过多线程就可以完成多客户端访问了。在客户端 new Socket 成功的时候,其实操作系统层面已经建立好了连接(TCP 三次握手),但是应用程序并没有接通这个连接。

多客户端回显服务线程池版本

既然可以通过多线程来实现多客户端,那么也可以实现线程池版本。只要把 new 线程的对方,改成线程池就好了。

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) {
            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());
        //处理请求和响应
        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] 客户端断开连接",clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    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 {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

运行结果如下:
客户端1:
在这里插入图片描述
客户端2:
在这里插入图片描述
客户端3:
在这里插入图片描述
服务器端:
在这里插入图片描述

实现翻译服务

和 UDP 版本的实现方法一样,不过是变成了 TCP 的模式。还是继承自线程池,代码如下:

public class TcpDictServer extends TcpThreadPoolEchoServer {
    private HashMap<String, String> dict = new HashMap<>();
    public TcpDictServer(int port) throws IOException {
        super(port);

        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("pig", "小猪");
    }

    @Override
    public String process(String request) {
        return  dict.getOrDefault(request,"没有此翻译");
    }

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

运行结果如下:
在这里插入图片描述

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lockey-s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值