TCP UDP协议要点整理

概述

        TCP和UDP是传输层中的两个非常重要的协议,这两个协议对应的网络通信api(socket api)也有较大差异.

        简单来说,TCP的特点是:有连接,可靠传输,面向字节流,全双工.而UDP的特点是:无连接,不可靠传输,面向数据报,全双工.

        有无连接:如果通信的前提是要先建立连接(如打电话),我们称之为有连接;反之(如发微信),我们称之为无连接.

        是否可靠传输:如果数据发送方能够知道接收方是否受到了其发送的数据,我们称之为可靠传输;反之,称之为不可靠传输.值得一提的是,在网络传输中,是无法保证百分之百的可靠传输的,极端情况下,假如网线断了,这就从物理层层面阻止了网络传输.

        面向字节流/数据报:发送/接受数据以字节为单位,我们称之为面向字节流;以数据报为单位则称之为面向数据报.

UDP部分

        基本类

                UDP协议下主要有两个常用的Socket API.

                1.DatagramSocket

                        这里需要明确的是,socket类,本质上也是"文件",打开一个socket文件同样会占用进程文件描述符表里的一个位置.socket文件对应到网卡设备.构造一个DatagramSocket对象,就相当于是打开了一个内核中的socket文件.

                        打开socket的文件之后,我们就可以借此实现端与端之间的数据传输了.有以下三个基本方法:

                        send():发送数据.

                        receive():接收数据.

                        close():关闭socket文件资源.

                2.DatagramPacket

                        DatagramPacket表示一个UDP数据报.UDP协议传输数据就是以这个作为基本单位.

        回显服务器(echo server)

                服务器                        

package network;

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

public class UdpEchoServer {
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动:");
        while(true){
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            String response = process(request);
            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 udpEchoServer = new UdpEchoServer(8000);
        udpEchoServer.start();
    }
}

                要点详解:

                        1.

private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
    socket = new DatagramSocket(port);
}

                        此处创建一个构造方法,手动给服务器绑定一个端口号(port),方便客户端去访问.

                        一个操作系统上,有很多端口号,范围为0-65535.程序如果需要进行网络通信,就需要获取到一个端口号.端口号相当于网络中用于区分不同进程之间的标识符.(操作系统收到网卡的数据,就可以根据网络数据报中的端口号,来确定要把数据发送给哪个进程).

                        一个端口在通常情况下只能绑定一个进程,而一个进程可以同时绑定多个端口.如果尝试将一个进程与一个已经被占用的端口绑定,会直接抛出异常.

                        2.

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

                        此部分是UDP服务器接收数据的逻辑.由于UDP协议是面向数据报的协议,而客户端发送过来的数据通常是字符串.所以我们需要人为把客户端发送过来的数据封装成一个数据报.上述代码就是构造了空的数据报,用于将接收过来的字符串封装成一个UDP数据报(我们称这种参数为输出型参数).观察此处构造函数中的参数可以看出,DatagramPacket本质上就是一个字节数组.

                        此外,若服务器在启动后始终没有接收到客户端发来的数据,那么socket.receive()方法将会阻塞等待.

                        3.

String request = new String(requestPacket.getData(),0,requestPacket.getLength());
String response = process(request);
DatagramPacket responsePacket = new DatagramPacket (response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);

                           这部分是数据的解析与发送.

                           服务器利用receive中获得的数据报.可以重构回客户端发送的字符串数据(即getData()与getLength()).然后通过process()函数得到需要发送回给客户端的字符串数据.

                           然后就到了封装UDP数据报的部分.数据报的内容和长度我们可以通过String类中的方法确定(即getBytes()和getBytes().length).然后我们还需要确定这个数据报需要发送给谁.这部分有关于客户端的信息可以通过UDP数据报的getSocketAddress()方法获得(注意是request的).由此,我们就构造好了responsePacket.将其通过send()方法发送出去即可.

                            此处还需要注意的一点是,由于是回显服务器(echo server).服务器不需要对客户端发送的请求做任何处理,原本返回即可.

                        4.

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

                           这部分代码是在进程中创建UDP服务器的实例.值得一提的是,我们不推荐给服务器分配号码数小于1024的端口号,因为小于1024的端口号通常是操作系统保留的.

                客户端         

package network;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    public UdpEchoClient() throws SocketException {
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while  (true){
            System.out.print("<<");
            String request = scanner.next();
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName("127.0.0.1"), 8000);
            socket.send(requestPacket);
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0, responsePacket.getLength());
            System.out.printf("req: %s; resp: %s\n",request,response);
        }
    }

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

                        要点详解:

                                1.

private DatagramSocket socket = null;
public UdpEchoClient() throws SocketException {
    socket = new DatagramSocket();
}

                               注意到客户端DatagramSocket的构造不需要手动传入端口号,而是选择让用户的操作系统自行分配.这是因为作为开发者,我们无法知道用户的机器上有哪些端口是空闲的,让操作系统自行分配可以避免异常. 

                                2.

String request = scanner.next();
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName("127.0.0.1"), 8000);
socket.send(requestPacket);

                                此处是客户端的数据封装与发送.我们可以注意到此处UDP数据报的构造方式与服务器部分的不同:客户端在构造数据报时,需要带上自己的IP地址和服务器的端口号.这两者相当于快递的寄件方和收件方.而服务器在构造数据报时,只需要明确接收方即可.(此处客户端本地的地址获取调用了InetAddress类中的getByName()方法,"127.0.0.1"是自己主机的IP地址)

                                然后我们通过send()方法将数据报发送给服务器即可.

                                3.接收数据部分的逻辑与服务器一致,在此不再赘述.

TCP部分

        基本类

                1.ServerSocket:服务器使用的socket

                 2.Socket:服务器和客户端都会使用的socket     

                 这两者的应用在后面会讲到.

        回显服务器(echo server)

                服务器 a  

package network;

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 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 socket = serverSocket.accept();
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(socket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    public void processConnection(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()){
                    System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                String request = scanner.next();
                String response = process(request);
                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 {
            clientSocket.close();
        }
    }

    public String process(String resp){
        return resp;
    }

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

                    要点介绍:

                        1.TCP服务器的构造与UDP类似,都需要手动指定端口号,方便客户端访问.

                        2.

ExecutorService service = Executors.newCachedThreadPool();
while(true){
      Socket socket = serverSocket.accept();
      service.submit(new Runnable() {
           @Override
           public void run() {
                try {
                    processConnection(socket);
                 } catch (IOException e) {
                   e.printStackTrace();
              }
           }
      });
}

                          这部分与UDP服务器的差异较大.在UDP服务器中,服务器无需与客户端建立连接,只需要不断尝试接收客户端可能会发送的信息即可.

                          而在TCP服务器中,客户端和服务器需要建立连接,在建立连接之后才能继续数据通信.这个部分依赖于先前提到的Serversocket类中的accept()方法.该方法相当于获取到了客户端的socket文件,然后直接通过客户端的socket文件进行进一步操作.

                        3.    

try(InputStream inputStream = clientSocket.getInputStream();
     OutputStream outputStream = clientSocket.getOutputStream()) {
     Scanner scanner = new Scanner(inputStream);
     PrintWriter printWriter = new PrintWriter(outputStream);
     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.println(response);
          printWriter.flush();
          System.out.printf("[%s:%d] req: %s; resp: %s\n",
                        clientSocket.getInetAddress().toString(), clientSocket.getPort(),
                        request, response);
      }
}

                        这部分是通过获取的客户端的socket文件,进一步获得客户端传输过来的数据.我们前面提到过TCP协议是通过字节流来传输信息的,所以在对应的socket文件里,我们可以获得对应的输入输出流(即getInputStream()和getOutputStream()方法).

                        为了方便读写数据,我们可以把输入输出流用Scanner和PrintWriter进一步封装.然后,我们可以通过scanner.next()方法将客户端socket文件中的数据读取出来,解析后使用printWriter.println()方法将结果写回给客户端.

                        此处使用flush()清空缓冲区是防止返回的数据进入了缓冲区,没有及时发送给客户端.

                客户端                     

package network;

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 {
        socket = new Socket("127.0.0.1",8000);
    }

    public void start(){
        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){
                String request = scanner.next();
                printWriter.println(request);
                printWriter.flush();
                String response = scannerNet.next();
                System.out.printf("req: %s; resp: %s\n", request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

                要点介绍:

                        1.客户端方面,我们可以发现客户端的收发逻辑和服务器几乎完全一致,这也是TCP协议和UDP协议的区别之一.

一些补充

           端口进程查找

                   当我们发现想使用的某个端口被占用而我们不想换端口号时,我们可以在cmd下使用netstat -ano | findstr 8000 命令(这是一个管道复合命令)查找出占用了对应端口号的进程id,示例如图:

                    如上图所示,我们可以查找出当前主机下,占用了8000端口进程的进程id是12032.

              多线程的使用

                        可以注意到在TCP服务器的实现中使用了线程池.这是由TCP服务器的实现逻辑决定的.可以看到在TCP服务器的processConnection()方法中,有一个死循环,这个死循环会不断读取客户端发送过来的数据(数据可能是一次大数据,也可能是多次小数据),直到读到EOF(如连接中断时就会读到).

                        这就导致如果在单线程的情况下,一旦一个客户端-服务器连接建立且不中断,服务器会永远阻塞在processConnection()方法中,无法与其他的客户端建立连接,这显然是无法接受的.对此,我们引入多线程:即每来一个客户端,我们就另起一个线程执行processConnection()方法.这就把accept()方法和后续的数据处理隔绝开来,实现了多客户端.采用线程池的原因是考虑到可能会有较大规模的线程的创建和销毁,线程池能节约系统资源.

                         实际上,我们也可以人为的约定数据的传输格式,手动在客户端中调用close()方法(如每发送一个字符串,中断一次) ,这样可以绕开多线程.

                         至于UDP协议,由于UDP协议下,传输数据不需要建立连接,所以也就不会存在"服务器等待客户端传输数据从而导致阻塞"这个问题,UDP服务器只需要不断接受数据即可,而服务器处理数据的速度是极快的,基本上可以看成是并发执行,也就没必要使用多线程.             

            

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值