一、UDP和TCP

       Socket API 是操作系统给应用程序提供的来进行网络数据的 发送和接收的api(即传输层给应用层使用的api)。
在需要通过操作系统来执行的传输层里,提供了两个最核心的协议:UDP和TCP。因此Socket API也提供了两种风格:UDP、TCP。下面我们来看看UDP和TCP两种方式有什么区别。

TCP:有连接,可靠传输,面向字节流,全双工。
UDP:无连接,不可靠传输,面向数据报,全双工。

二、Udp版本客户端服务器

1、DatagramSocket和DatagramPacket(数据报)

DatagramSocket类的相关方法:
构造方法:

  • 进程关联了端口号,本质上是进程里的Socket对象关联了 端口号。同时一个进程可以创建多个Socket对象,每个Socket对象都可以连接到不同的网络地址和端口。因此一个进程可以关联多个端口,但一个端口只能关联一个进程。

普通方法:

  • receive方法中的DatagramPacket是我们创建的传入的一个空的对象,当receive接收到发送方发来的数据报时,才把发送方发来的内容填充进入我们传入的这个空的DatagramPacket对象,得到接收到的数据报,这个参数也叫做"输出型参数"。

DatagramPacket类(数据报)的相关方法:
构造方法:

普通方法:

2、UdpEchoSever&&UdpEchoClient

2.1、什么是Echo Sever?

2.2、UDP客户端+UDP回显服务器代码

客户端

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

public class UdpEchoClient {
    private DatagramSocket socket = null;
    // 客户端的ip是环回ip(127.0.0.1),端口是操作系统随机分配的一个端口
    // 因为在本机模拟通信,所以服务器的ip也是环回ip(127.0.0.1),端口是程序员指定的
    // 服务器的ip和端口都得告诉客户端,我们才能在客户端访问服务器
    private String serverIp = null;
    private int serverPort = 0;

    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }
    public void start() throws IOException {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        while (true){
            // 1.从控制台读取数据到一个空的DatagramPacket中
            System.out.print("> ");
            String request = scanner.next();
            if(request.equals("exit")){
                System.out.println("客户端关闭!");
                break;
            }
            
            // 注意1:InetAddress.getByName(serverIp)操作把点分十进制的ip(127.0.0.1)转换成32位二进制数
            // 注意2:发送数据报时,使用String的getBytes().length方法获取数据报长度
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length
                                           ,InetAddress.getByName(serverIp), this.serverPort);
            // 2.把DatagramPacket发给服务器
            socket.send(requestPacket);
            
            // 3.使用空的DatagramPacket,接收服务器处理后的响应数据
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);  // 注意:如果receive没有接收到响应数据,那就会阻塞等待。

            // 4.打印响应结果
            // 注意1:打印返回的响应结果,不能用toString,因为你无法为DatagramPacket类重写toString方法
            // 注意2:接收数据报时使用DatagramPacket的getLength方法获取数据报长度
            String response = new String(responsePacket.getData(),0, responsePacket.getLength());
            System.out.println(response);
        }
    }
    public static void main(String[] args) throws IOException {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

服务器

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

public class UdpEchoServer {
    // 注意:1.这个socket对象在操作系统内核中操作时,是当成文件的方式操作,把这个对象当成网卡的抽象
    private DatagramSocket socket = null;
    // 注意:2.服务器端需要手动指定一个端口,避免客户端找不到服务器
    public UdpEchoServer(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true){
            // 1.给一个空的DatagramPacket,用于接收客户端发来的数据报
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);   // 注意:如果receive没有接收到请求数据,那就会阻塞等待。
            
            // 注意1:为了便于处理,把DatagramPacket这个特殊的对象转化成字符串的形式,但是不能用toString,因为你无法为DatagramPacket类重写toString方法
            // 注意2:接收数据报时使用DatagramPacket的getLength方法获取数据报长度
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            // 2.对请求内容进行业务处理(这里是回显服务器直接返回)
            String response = process(request);
            
            // 3.构造好响应的DatagramPacket,并把它发回客户端。
            // (注意1:这里也可以直接使用requestPacket.getSocketAddress()同时获取IP和端口,客户端的端口和ip是requestPacket自带的。
            //  注意2:第二个参数必须是字节数组长度response.getBytes().length,而不是字符串的长度
            //        使用String的getBytes().length方法获取数据报长度)
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length
                    ,requestPacket.getAddress(),requestPacket.getPort());
            socket.send(responsePacket);

            // 4.为了观察,打印一下客户端发来的的信息
            System.out.printf("[%s,%d] req:%s; resp:%s\n",requestPacket.getAddress(),requestPacket.getPort()
                                                          ,request, response);
        }
    }

    private String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        UdpEchoServer sever = new UdpEchoServer(9090);
        sever.start();
    }
}

执行顺序:

1.服务器先启动,进行到receive进行阻塞,等待客户端发送请求数据报(服务器)
2.客户端读取用户输入内容到请求数据报(客户端)
3.客户端执行send把请求数据报发给服务器(客户端)
4.客户端发送请求数据报后立即执行到receive,等待服务器发来响应数据报(客户端)
  服务器接收到请求数据报,从服务器的receive阻塞中返回(服务器)
5.服务器根据请求数据报计算响应数据报(服务器)
6.服务器执行send,发送响应数据报给客户端(服务器)
7.客户端从receive阻塞中返回,读到响应数据报(客户端)

2.3、查词典服务器代码

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

public class UdpDictSever extends UdpEchoServer{
    // 使用一个集合来存放单词集合
    private Map<String,String> dict = new HashMap<>();

    public UdpDictSever(int port) throws SocketException {
        super(port);
        dict.put("cat","猫");
        dict.put("beautiful","美丽的");
        dict.put("perfect","完美的");
    }

    @Override
    public String process(String request){
        return dict.getOrDefault(request,"没有你要查的单词!");
    }

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

三、Tcp版本客户端服务器

1、ServerSocket和Socket

ServerSocket类的相关方法:
构造方法:

普通方法:

Socket类的相关方法:
构造方法:

普通方法:

2、TcpEchoServer&&TcpEchoClient

2.1、Tcp客户端

Tcp版本的客户端和Udp版本的客户端的区别:

客户端:

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 {
        // 注意1:在客户端new一个Socket对象的时候,就连接服务器。
        // 注意2:Socket对象可以字节把点分十进制的serverIp转换成32位二进制数
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){
        System.out.println("客户端启动!");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            Scanner scanner = new Scanner(System.in);
            while (true){
                // 1.客户端从控制台读取用户输入的内容
                System.out.print(">");
                String request = scanner.next();
                if (request.equals("exit")){
                    System.out.println("客户端关闭!");
                    break;
                }

                // 2.客户端把请求写入网卡,发送给服务器处理
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);   //注意:要写入"\n"
                printWriter.flush();  // 冲刷,保证数据写入网卡

                // 3.客户端读取服务器响应写回到网卡上的数据
                Scanner respScan = new Scanner(inputStream);
                String response = respScan.next();

                System.out.println(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();
    }
}
客户端代码步骤:
1. 客户端从控制台读取用户输入的内容
2. 客户端把请求写入网卡,发送给服务器处理
3. 客户端读取服务器响应写回到网卡上的数据
4. 打印响应的结果

2.2、Tcp服务器

Tcp版本的服务器和Udp版本的服务器的区别:

Tcp版本的服务器需要注意的点:

  • 1> Tcp版本的服务器需要在发送消息时在数据后面加上\n。因为接收端读取数据时使用Scanner的next方法读取,next方法规则是:读到换行符/空格/tab时结束,读到的数据不包含以上符号。所以发送端可以在数据的结尾加上\n,表示读取数据结束。这个点客户端也是一样。如下图:printWriter.println(outputStream)表示在发送数据outputStream后面加上一个\n。发送outputStream后,一定记得flash,把信息真正的发送。
    【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器-LMLPHP
    【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器-LMLPHP

  • 2> 在Tcp版本的服务器端中,需要关闭客户端访问时创建的Socket资源。每次有一个客户端访问服务器,就会创建一个Socket对象和客户端的Socket连接。服务器端每创建一个Socket对象,就在服务器的这个进程上的文件描述符表上占用一个空间,而客户端访问量应该是很多的。因此如果连接完成后,不关闭这个Socket,到了文件描述符表位置被占满时,其它客户端就无法再访问服务器了,因此,在每个客户端连接完成后,我们需要关闭服务器端的这个Socket资源,释放这个Socket占用的文件描述符表的位置。
    那么为什么Udp版本的服务器不需要关闭?Udp版本服务器端的的DatagramSocket的生命周期是整个进程。而Tcp版本的clientSocket的生命周期是每个客户端连接时,断开连接,这个Socket就没用了,且因为每创建一个客户端连接,服务器就会创建一个clientSocket,所以数量上也会很多!

  • 3> 短连接和长连接:下列代码的processConnection中的while去掉就是短连接,即传输一次就断开连接,每次访问都得先连接再发送请求;长连接即用while,当一个客户端连接好服务器然后发送请求后,先不断开连接,等待用户再次发送请求,等用户自己退出时才断开连接。
    【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器-LMLPHP

  • 4> IO多路复用,如果客户端访问量很大,即使使用多线程服务器压力还是很大,就需要用IO多路复用。比如C10K问题(1w个客户端),C10M问题(1kw个客户端访问)。IO多路复用,可以使用一个线程处理多个客户端的任务。原理:在这个线程中使用一个集合来存放连接对象,这个线程就负责监听这个集合,在集合中哪个连接有数据来了,线程就处理这个连接。在操作系统中提供了select,epoll就可以监听。

服务器:

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 TcpEchoSever {
    private ServerSocket serverSocket = null;
    // 注意:服务器本身使用ServerSocket和端口绑定连接
    public TcpEchoSever(int Port) throws IOException {
        serverSocket = new ServerSocket(Port);
    }


    public void start() throws IOException {
        System.out.println("启动服务器!");
        // 注意:使用while保证每次有客户端连接时都能连接到
        while (true){
             版本一:使用多线程
//            // 注意:每当有一个客户端连接服务器时,创建一个Socket对象和客户端的Socket进行通信
//            Socket clientSocket = serverSocket.accept();
//            // 注意:建立连接使用当前线程,放在我们创建的线程外;使用多线程去处理客户端发来的请求(处理业务)
//            Thread t = new Thread(()->{
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            t.start();

            // 版本二:使用线程池
            Socket clientSocket = serverSocket.accept();
            ExecutorService pool = Executors.newCachedThreadPool();
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    // 注意:一个连接对应一个客户端,
    private void processConnection(Socket clientSocket) throws IOException {
        // 注意:服务器的每一个Socket对应一个客户端
        System.out.printf("[%s:%d] 客户端上线!\n",
                clientSocket.getInetAddress().toString(),
                clientSocket.getPort());

        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){
            // 注意:由于一个客户端可能要处理多个请求和响应,所以使用循环进行
            while (true){
                // 1.服务器读取客户端写入网卡的字节流数据
                Scanner reqScan = new Scanner(inputStream);
                if (!reqScan.hasNext()){
                    System.out.printf("[%s:%d] 客户端下线!\n",
                            clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                String request = reqScan.next();
                // 注意:next读到换行符/空格/tab结束,但是读取的内容不包含换行符/空格等
                //    我们这里是从客户端的请求内容就读取,所以客户端发来的请求中应当有以上结束符

                // 2.对请求进行业务处理
                String response = process(request);

                // 3.服务器把响应内容写回网卡,响应给客户端
                //   操作:用outputStream构造一个PrintWriter字符流对象,便于把"\n"一并写入网卡
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();   // 冲刷,保证数据写入网卡

                // 4.打印日志
                System.out.printf("[%s:%d] req:%s; resp:%s\n",
                        clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }
    }

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

    public static void main(String[] args) throws IOException {
        TcpEchoSever sever = new TcpEchoSever(9090);
        sever.start();
    }
}
服务器代码步骤:
1. 服务器读取客户端写入网卡的字节流数据
2. 对请求进行业务处理
3. 服务器把响应内容写回网卡,响应给客户端

执行顺序:

1.服务器先启动,进行到accept进行阻塞,等待客户端new Socket从而建立连接(服务器)
2.客户端从控制台读取用户输入内容(客户端)
3.客户端使用OutputStream把请求发给服务器(客户端)
4.服务器Socket感知到请求并使用InputStream接收请求(服务器)
5.服务器根据请求计算响应(服务器)
6.服务器使用OutputStream把响应发回客户端(服务器)
7.客户端Socket感知到请求并使用InputStream接收请求(客户端)
8.客户端打印响应结果

三、UDP和TCP总结

【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器-LMLPHP
【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器-LMLPHP

04-18 16:24