JavaEE初阶学习:网络编程

1.网络编程套接字

网络编程套接字就是操作系统给应用程序提供的一组API(叫做socket API)。

socket 可以视为是应用层和传输层之间的通信桥梁。

传输层的核心协议有两种:TCPUDP

TCP:有连接;可靠传输;面向字节流;全双工;

UDP:无连接;不可靠传输;面向数据报;全双工;

  1. 有连接和无连接
    有连接:像打电话,得先接通,才能交互数据
    无连接:像发微信,不需要接通,直接就能发数据

  2. 可靠传输与不可靠传输
    可靠:传输过程中,发送方知道接收方有没有收到数据
    不可靠:参考无连接,微信直接可以发消息,不知道对方有没有看见这个消息。

  3. 面向字流/数据报
    面向字节流:以字节为单位进行传输,类似于文件操作中的字节流。
    面向数据报:以数据报为单位进行传输,一个数据报会明确大小,一次发送/接收一个完整的数据报,不能是半个数据报。

  4. 全双工/半双工
    全双工:一条链路,双向通信。
    半双工:一条链路,单向通信。

2.UDP Socket

UDP Socket 中主要涉及两个类:DatagramSocket 和 DatagramPacket.

DatagramSocket 的一个对象就对应操作系统中的一个socket文件。

构造方法:
JavaEE初阶学习:网络编程_第1张图片
方法:
JavaEE初阶学习:网络编程_第2张图片

DatagramPacket代表了一个UDP数据报,使用UDP传输数据的基本单位。

构造方法:
JavaEE初阶学习:网络编程_第3张图片

方法:
JavaEE初阶学习:网络编程_第4张图片

构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建

JavaEE初阶学习:网络编程_第5张图片

1.客户端服务器程序-回显服务(EchoServer)

回显:请求内容是啥,得到的响应就是啥。

1.UdpEchoServer

  1. 进行网络编程,第一步就需要先准备好 socket实例,这是进行网络编程的大前提
private DatagramSocket socket = null;
  1. 此处在构造服务器这边的socket对象的时候,就需要显式的绑定一个端口号端口号是用来区分一个应用程序的,主机收到网卡上的数据的时候,这个数据该给哪个程序?
    抛出异常的原因:
    1.端口号可能已经被占用了。
    2.每个进程能够打开的文件个数是有限的。
public UdpEchoServer(int port) throws SocketException {
    socket = new DatagramSocket(port);
}
  1. 启动服务器,服务器是被动接收请求的一方,主动发送请求的是客户端,DatagramPacket刚才说过是表示一个UDP数据报,发送一次数据就是发送DatagramPacket,接收一次数据也就是在收一个DatagramPacket。
// 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);

        }
    }

    // 由于是回显服务,响应就和请求一样了
    // 实际上对于一个真实的服务器来说,这个过程是复杂的,为了实现这个过程,可能需要几行甚至几万行
    private String process(String request) {
        return request;
    }

  • 这里requestPacket.getLength()这个长度不一定是1024,可能此处的UDP数据报最长是1024,实际的数据可能不够1024。
  • 注意这里send方法的参数,也是DatagramPacket,需要把响应数据先构造成一个DatagramPacket再进行发送。
  • response.getByte()这里的参数也不再是一个空的数组,response是刚才根据请求计算得到的响应。DatagramPacket里面的数据就是String response的数据。
  • requestPacket.getSocketAddress();这个参数的作用就是表示要把数据发给哪个地址+端口。
  • 完整代码:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

@SuppressWarnings({"all"})
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("启动服务器");
        // UDP 不需要建立连接,直接接收从客户端来的数据即可~
        while (true) {
            // 1. 读取客户端发来的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
            socket.receive(requestPacket);// 为了接收数据,需要先准备一个空的DatagramPacket对象,由receive来进行填充数据
            // 把 DatagramPacket 解析成一格String
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "utf-8");
            // 2. 根据请求计算响应( 由于是一个回显服务器,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);

        }
    }

    // 由于是回显服务,响应就和请求一样了
    // 实际上对于一个真实的服务器来说,这个过程是复杂的,为了实现这个过程,可能需要几行甚至几万行
    private String process(String request) {
        return request;
    }

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

2.UdpEchoClient

在客户端构造 socket 对象的时候,就不再手动指定端口号,使用无参版本的构造方法。
不指定端口号,是让操作系统自己分配一个空闲的端口号。
JavaEE初阶学习:网络编程_第6张图片


import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;

@SuppressWarnings({"all"})
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    public UdpEchoClient(String ip, int port) throws SocketException {
        socket = new DatagramSocket();
        serverIp = ip;
        serverPort = port;
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 先从控制台读取一个用户输入的字符串
            System.out.println("-> ");
            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);
            // 4. 把响应结果显示到控制台上。
            String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
            System.out.printf("req: %s, resp: %s\n",request,response);

        }
    }

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

3.翻译程序

package network;

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;

@SuppressWarnings({"all"})
public class UdpDictServer extends UdpEchoServer{

    private HashMap<String, String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);

        // 简单构造几个词
        dict.put("cat","小猫");
        dict.put("dog","小狗");
        dict.put("hello","你好");
        dict.put("pig","小猪");
    }

    @Override
    public String process(String request) {
        return dict.getOrDefault(request, "该词无法被翻译");
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}
public class TcpDictServer extends TcpThreadPoolEchoServer{
    // 定义一个字典
    private Map<String, String> dict = new HashMap<>();

    public TcpDictServer(int port) throws IOException {
        super(port);
        // 构造字典的内容
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("pig", "小猪");
        dict.put("haha", "哈哈");
    }

    @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();
    }
}

3.TCP API

在TCP API 中,也是涉及到两个核心的类,
ServerSocket(专门给TCP服务器用)
Socket(既需要给服务器用,又需要给客户端用)

前面基本都和UDP的差不多,区别就是这里的start方法,由于tcp是有连接的,不是一上来就能读数据,需要先建立连接(打电话);accept返回了一个socket对象,accept可以理解为接电话,那么接电话的前提就是有人给你打电话,如果无,那么这里accept就会阻塞。

TcpEchoServer

// listen -> 监听
    // 但是在 Java socket 中是体现不出来"监听"的含义的
    // 之所以这么叫,是操作系统原生的API 中有一个操作叫做listen
    private ServerSocket listenSocket = null;

    public TcpEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 由于 Tcp是有连接的,不能一上来就读取数据,而要先建立连接(接电话)
            // accept 就是在"接电话",前提是有人给你打电话
            // accept 返回了一个socket 对象,称为clientSocket,后续和客户端的沟通都是通过clientSocket来完成的
            // 进一步讲,listenSocket就干了一件事,就是接电话
            Socket clientSocket =  listenSocket.accept();
            processConnection(clientSocket);
        }
    }

接下来我们来写processConnection()方法的代码。

private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端建立连接!", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        // 接下来处理请求和响应
        // 这里的针对 TCP socket 的读写就和文件读写是一模一样的
        try (InputStream inputStream = clientSocket.getInputStream()) {
            try (OutputStream outputStream = clientSocket.getOutputStream()) {
                // 循环的处理每个请求,分别返回响应
                Scanner scanner = new Scanner(System.in);
                while (true) {
                    // 1. 读取请求
                    if (!scanner.hasNext()) {
                        System.out.printf("[%s:%d] 客户端断开连接!", 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) {
            throw new RuntimeException(e);
        } finally {
            clientSocket.close();
        }
    }

    private String process(String request) {
        return request;
    }

完整代码:



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;

@SuppressWarnings({"all"})
public class TcpEchoServer {
    // listen -> 监听
    // 但是在 Java socket 中是体现不出来"监听"的含义的
    // 之所以这么叫,是操作系统原生的API 中有一个操作叫做listen
    private ServerSocket listenSocket = null;

    public TcpEchoServer(int port) throws IOException {
        listenSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 由于 Tcp是有连接的,不能一上来就读取数据,而要先建立连接(接电话)
            // accept 就是在"接电话",前提是有人给你打电话
            // accept 返回了一个socket 对象,称为clientSocket,后续和客户端的沟通都是通过clientSocket来完成的
            // 进一步讲,listenSocket就干了一件事,就是接电话
            Socket clientSocket = listenSocket.accept();
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端建立连接!", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        // 接下来处理请求和响应
        // 这里的针对 TCP socket 的读写就和文件读写是一模一样的
        try (InputStream inputStream = clientSocket.getInputStream()) {
            try (OutputStream outputStream = clientSocket.getOutputStream()) {
                // 循环的处理每个请求,分别返回响应
                Scanner scanner = new Scanner(System.in);
                while (true) {
                    // 1. 读取请求
                    if (!scanner.hasNext()) {
                        System.out.printf("[%s:%d] 客户端断开连接!", 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) {
            throw new RuntimeException(e);
        } 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();
    }
}

TcpEchoClient


import jdk.internal.util.xml.impl.Input;
import org.omg.Messaging.SYNC_WITH_TRANSPORT;

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

@SuppressWarnings({"all"})
public class TcpEchoClient {
    // 用普通的 socket 即可,不用 ServerSocket了
    // 此处也不用手动给客户端指定端口号,让系统自由分配
    private Socket socket = null;

    public TcpEchoClient(String sreverIP, int sreverPort) throws IOException {
        // 其实这里是可以给的,但是这里给了之后,含义是不同的
        // 这里传入的ip和端口号的含义表示的不是自己绑定,而是表示和这个ip端口建立连接口
        socket = new Socket(sreverIP, sreverPort);
    }

    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(System.in);
                    String response = respScanner.next();
                    // 4. 把结果显示到控制台上
                    System.out.printf("req: %s, resp: %s\n",request, response);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

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


JavaEE初阶学习:网络编程_第7张图片
JavaEE初阶学习:网络编程_第8张图片
那么我们结束客户端,看服务器这边的响应。

JavaEE初阶学习:网络编程_第9张图片
在这里插入图片描述
我们可以发现这里的代码第一次accept结束之后,就会进入processConnection,在processConnection中又有一个循环,若processConnection里面的循环不停,processConnection就无法完成,就会导致外层循环无法进入下一轮,也就无法第二次调用accept了。

我们可以通过多线程的方式来解决这个问题,让主线程循环调用accept,当有客户端连接上来的时候就让主线程创建一个新线程,由新线程负责客户端的若干个请求,这个时候多个线程看上去是同时执行的。

这里我们新写一个类TcpThreadEchoServer,在原有的TcpEchoServer基础上修改以下部分代码即可。

     Thread t = new Thread(()->{
         processConnection(clinetSocket);
     });
     t.start();

TcpThreadEchoServer

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;
    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();
            Thread t = new Thread(()->{
                processConnection(clientSocket);
            });
            t.start();
        }
    }

    private void processConnection(Socket clinetSocket) {
        System.out.printf("[%s:%d客户端建立连接!\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
        try(InputStream inputStream = clinetSocket.getInputStream()){
            try(OutputStream outputStream = clinetSocket.getOutputStream()){
                Scanner scanner = new Scanner(inputStream);//读取请求
                while (true){
                    if(!scanner.hasNext()){
                        System.out.printf("[%s:%d]客户端断开连接!\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
                        break;
                    }
                    String request = scanner.next();
                    String response = process(request);
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    //刷新缓冲区,如果没有这个刷新,可能客户端就不能第一时间看到响应结果。
                    printWriter.flush();
                    System.out.printf("[%s:%d] rep:%s,resp:%s\n",
                            clinetSocket.getInetAddress().toString(),clinetSocket.getPort(),request,response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //记得关闭操作
            try {
                clinetSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String process(String request) {
        return request;
    }

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

TCP 版本的线程池服务器

public class TcpThreadPoolEchoServer {
    // 用于服务器的Socket
    ServerSocket socket;

    // 构造方法指定端口号
    public TcpThreadPoolEchoServer(int port) throws IOException {
        socket = new ServerSocket(port);
    }

    // 启动服务
    public void start() throws IOException {
        System.out.println("服务已启动.");
        // 创建一个线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        while (true) {
            // 服务器启动后就开始接收客户端连接
            Socket clientSocket = socket.accept();
            // 处理接收,建立连接后就把他加入到线程池里去
            threadPool.submit(() -> {
                processConnections(clientSocket);
            });
        }

    }

    private void processConnections(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 scanner = new Scanner(inputStream);
                // 循环获取请求
                while (true) {
                    // 如果没有下一个数据就结束
                    if (!scanner.hasNext()) {
                        System.out.printf("[%s:%d] 客户端断开连接.\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                        break;
                    }
                    String request = scanner.next();
                    // 处理数据
                    String response = process(request);
                    // 把处理结果响应给客户端
                    // 为了方便起见用PrintWriter把outputStream
                    PrintWriter writer = new PrintWriter(outputStream);
                    writer.println(response);
                    // 强制刷新缓冲区
                    writer.flush();
                    // 打印日志
                    System.out.printf("[%s:%d] request : %s, response : %s\n", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(), request, response);
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                // 关闭Socket
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public String process(String request) {
        // echo直接返回
        return request;
    }

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

那么之前为什么UDP版本的程序就不需要多线程就可以处理多个请求呢?

因为UDP不需要连接,只需要一个循环就可以处理所有客户端的请求,但是TCP即需要处理连接,又需要处理一个连接中的多个请求。

你可能感兴趣的:(JavaEE的初阶学习,网络,java-ee,学习)