【Java 基于Socket的UDP和TCP编程】简单理解

一、前言

我们知道负责运输层就是负责两个主机中进程之间的通信提供服务,由于一个主机可以运行多个进程,因此运输层有复用和分用的功能。复用就是多个应用进程可以同时使用下面的运输层的服务,分用则是运输层把收到的信息分别交给上面的应用层中的相应进程
运输层主要使用UDP和TCP两种协议,主要目的是将数据发送给给定的应用

二、UDP和TCP协议的特点及使用场景

UDP协议:
1、无连接
2、尽最大努力交付
3、面向报文
4、无拥塞控制
5、支持一对一,一对多,多对一,多对多的交互通信
6、首部开销小(四个字段:源端口,目的端口,长度,检验和)。UDP首部占用8个字节

UDP首部格式:
在这里插入图片描述
用户数据报UDP有两个字段:数据字段和首部字段。首部很简单,只有8字节。由四个字段组成,每个字段意义如下:
1、源端口:在需要对放回信时选用。不用时可用全0
2、目的端口:在终点交付报文时必须使用
3、长度:UDP用户数据报长度,其值最小为8(仅包含首部)
4、检验和:检验UDP用户数据报在传输中是否有错,有错就丢弃

TCP协议:
1、面向连接:通信之前必须要建立连接
2、每一条TCP只能点对点链接(一对一)
3、提供可靠的交付服务:通过TCP协议连接传输数据,无差错,不丢失,不重复
4、提供全双工通信
5、面向字节流。虽然程序和TCP交互是一次一个数据块,但是把应用程序交下来的数据仅仅看成的是一连串的无结构字节流
6、TCP首部占20字节

TCP首部格式:
在这里插入图片描述
各个字段的作用与含义:
1、源端口号和目的端口号
各占两个字节,我们知道端口号就是识别特定主机上的唯一进程,而ip地址是用来标识网络中的不同主机的,两个源(sourse)和目的(dst)端口号和IP首部中的源和IP地址,则标识互联网上的唯一进程,所以套接字的定义说白了就是:IP地址和端口号共同组成
2、确定序列
四个字节,上一个字段的序号是对数据的编号,所以确认序列号是下一个期望接受的TCP分段号,相当于对对方发送的并且已经被本方接收的分段确认。仅当ACK标志为1时有效。确认号表示期望收到的下一字节的序号
ack:期望下次收到的序号
ack怎么算的:通过收到的序号和数据长度相加。假设A收到B过来的数据(seq = 5,len = 15).len表示数据长度,那么A就会回复B,“刚才数据我收到了,你接下来就发序列号为20的包给我把”。这样就保证了不会乱序
3、ACK
当ACK为1时。确认号表示期望收到的下一字节的序号
4、序号
四个字节,表示在这个报文段中的第一个数据字节序号。如果将字节流看作两个应用程序的单向流动,则TCP用序号对每个字节进行计数。用来保证达到数据顺序的编号,所以这个字段是需要比加大的存储
5、位数据偏移
以32位(4字节)字长为单位,需要这个值是因为任选字段长度是可变的。跟IP头部一样,以4字节为单位。最大60字节。不存在任选字段正常的报头长度是20字节。其实相当于给出数据在数据段中的开始位置
6、保留位
6位,必须为0
7、标志位
占有6个比特位,他们中可以有多个位置为1,以此为:URG,ACK,PSH,RST,SYN,FIN
下面具体分析:
URG:该位置为1说明TCP包的紧急指针域有效,用来保证TCP链接不被中断,并且督促上层应用尽快处理这次数据
PSH:接收方会把这个报文尽快交给应用层,叫做push。所谓push操作就是把指在数据包到达接受端以后,立即传送给应用程序,而不是在缓冲区排队
8、RST
连接复位,复位因主机崩溃/别的原因出现的连接错误,也可以拒绝非法的分段请求
9、SYN
是一个同步序号,通常和ACK合用来建立连接,也就是通常说的三次握手
10、FIN
既然有建立连接那么必然有拆除连接,这个字段表示发送端已经到达数据末尾,也就是双方数据传送完成,没有数据传送,发送FIN标志位的TCP数据包后,连接断开。这个标志的数据包也经常用于进行端口扫描

UDP使用场景:
UDP由于不保证消息的可靠性,所以UDP适合发送一些不需要每条都保证无误的到达接收者
例如:视频通信

TCP使用场景:
由于TCP能够保证消息的可靠性,所以例如游戏信息,文字信息等适用于TCP通信

三、编程实践

在这里插入图片描述

1.UDP编程步骤

UDP使用数据报进行数据传输,没有客户端与服务器端之分,只有发送方与接收方,两者哪个先启动都不会报错,但是会出现数据丢包现象。发送的内容有字数限制,大小必须限制在64k以内

UDP客户端步骤:
1.构造DatagramSocket实例
2.通过DatagramSocket实例的send和receive方法收发DatagramPacket报文
3.调用DatagramSocket的close()方法关闭连接

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 UDPClient {
    private DatagramSocket socket;//开发YDP的工具包
    private DatagramPacket packet;//消息封装类
    private DatagramPacket sendpacket;//消息封装类
    private Scanner scanner;//控制台输出
    private final int port = 5676;
    private final String IP = "127.0.0.1";


    public UDPClient() {
        try {
            socket = new DatagramSocket();
            byte[]bytes = new byte[1024];
            //packet底层封装字节数组
            packet = new DatagramPacket(bytes,0,bytes.length);
            scanner = new Scanner(System.in);
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }
    public void startUDPClient(){
        //先给服务器发送消息
        try {
            while (true) {
                System.out.println("请输入信息:");
                String s = scanner.nextLine();
                byte[] bytes = s.getBytes();
                //packet底层封装字节数组
                sendpacket = new DatagramPacket(bytes, 0, bytes.length, InetAddress.getByName(IP), port);
                socket.send(sendpacket);

                if (s.equals("exit")){
                    break;
                }

                socket.receive(packet);
                String str = new String(packet.getData(), 0, packet.getLength());
                System.out.println("接收到的服务器信息:" + str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (socket!=null){
                socket.close();
                socket = null;
            }
        }
    }

    public static void main(String[] args) {
        new UDPClient().startUDPClient();
    }
}

UDP服务端步骤:
1.构造DatagramSocket实例,指定本地端口
2.通过DatagramSocket的send和receive方法接收和发送DatagramPacket
3.调用DatagramSocket的close()方法关闭


import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.Scanner;
/**
 * 服务器端
 */
public class UDPServer {
    private DatagramSocket socket;//开发YDP的工具包
    private DatagramPacket packet;//消息封装类
    private DatagramPacket sendpacket;//消息封装类
    private Scanner scanner;//控制台输出

    /**
     * 端口号:网络传输,通过端口号把信息传输到特定的程序(进程)上
     * 我们这里把端口号写死,因为我们每次运行时候,服务器端的端口号是不停的变的
     * 结果就是:客户端不好连接到服务器端
     */
    private final int port = 5676;

    public UDPServer() {
        try {
            //将服务器绑定到特定端口
            socket = new DatagramSocket(port);
            byte[]bytes = new byte[1024];
            //packet底层封装字节数组
            packet = new DatagramPacket(bytes,0,bytes.length);
            scanner = new Scanner(System.in);
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }

    //接受客户端发送的请求,作出反应,返回结果
    public void startUDPServer(){
        //接收消息
        try {
            while (true) {
                socket.receive(packet);
                String str = new String(packet.getData(), 0, packet.getLength());
                System.out.println("接收到的客户端信息:" + str);

                if (str.equals("exit")){
                    break;
                }
                //根据接受的消息 作出反应  处理后结果返回给客户
                System.out.println("请输入回复信息:");
                String s = scanner.nextLine();
                byte[] bytes = s.getBytes();
                //packet底层封装字节数组
                //socket 就是封装客户端发来的消息  它里面包含的方法可以让我们获取到客户端的
                //的ip地址和端口号 然后底下就可以让我们把客户端消息发过去
                sendpacket = new DatagramPacket(bytes, 0, bytes.length, packet.getAddress(), packet.getPort());
                socket.send(sendpacket);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (socket!=null){
                socket.close();
                socket = null;
            }
        }
    }

    public static void main(String[] args) {
        new UDPServer().startUDPServer();
    }
}

2.TCP编程步骤

TCP是基于字节流的传输层通信协议,所以TCP编程是基于IO流编程。

对于客户端,我们需要使用Socket类来创建对象。对于服务器端,我们需要使用ServerSocket来创建对象,通过对象调用accept()方法来进行监听是否有客户端访问
在这里插入图片描述

TCP客户端步骤:
1.构建Socket实例,通过指定的服务器地址和端口建立连接
2.利用Socket实例包含的InputStream和OutputStream进行数据读写
3.操作结束后调用socket实例的close方法关闭连接

import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class BIOCIient {
    private Socket socket;
    private Scanner scanner;
    private final int port = 5676;
    private final String IP = "127.0.0.1";
    private BufferedWriter writer;
    private BufferedReader reader;
    private OutputStream outputStream;
    private InputStream inputStream;
    private volatile boolean flag = true;

    public BIOCIient() {
        scanner = new Scanner(System.in);
        try {
            socket = new Socket(IP, port);  //请求和服务器建立连接
            //TCP三次握手的起点  只要这个socket 建立成功说明连接建立成功
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void startClient() {
        try {
            boolean closed = socket.isClosed();
            if (!closed) {
                outputStream = socket.getOutputStream();//负责发送消息
                inputStream = socket.getInputStream();// 负责接收消息
                writer = new BufferedWriter(new OutputStreamWriter(outputStream));
                reader = new BufferedReader(new InputStreamReader(inputStream));
                new Thread(new SendThread()).start();
                while (flag) {
                    String str = reader.readLine();
                    System.out.println("接受到服务器的信息为 : " + str);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
                inputStream.close();
                outputStream.close();
                reader.close();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    class SendThread implements Runnable {

        @Override
        public void run() {
            try {
                while (true) {
                    System.out.println("请输入信息:");
                    String s = scanner.nextLine();
                    writer.write(s + "\n");
                    writer.flush();
                    if (s.equals("exit")) {
                        flag = false;
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }


        }
    }

    public static void main(String[] args) {
        new BIOCIient().startClient();
    }
}

TCP服务端步骤:
1.构建一个ServerSocket实例,指定本地的端口,用于监听其连接请求
2.调用socket的accept()方法获得客户端的连接请求,通过accept()方法返回的socket实例,建立与客户端的连接
3.通过返回的socket实例来获得InputStream和OutputStream,进行数据的写入和读出
4.调用socket的close()方法关闭socket连接

//TCP协议代码
public class BIOServer {
    private ServerSocket socket;//开发TCP的工具包
    private final int port = 5676;


    public BIOServer() {
        try {
            //将服务器绑定到特定端口上
            socket = new ServerSocket(port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void startTCPServer() {
        //接收客户端的请求
        try {
            while (true) {  //不停的接收客户端请求
                Socket clientSocket = socket.accept();  //阻塞方法   阻塞到连接建立上为止
                //TCP 建立连接  三次握手   三次握手的终点
//            clientSocket请求连接的用户一一对应
                //后面讲使用该clientSocket进行和这个用户之间的通信
                new Thread(new ServerHandlder(clientSocket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        new BIOServer().startTCPServer();
    }
}

ServerHandlder:

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

public class ServerHandlder implements Runnable {
    private Socket clientSocket;
    private BufferedWriter writer;
    private BufferedReader reader;
    private OutputStream outputStream;
    private InputStream inputStream;
    private Scanner scanner;

    public ServerHandlder(Socket clientSocket) {
        scanner = new Scanner(System.in);
        this.clientSocket = clientSocket;
        try {
            outputStream = clientSocket.getOutputStream();//负责发送消息
            inputStream = clientSocket.getInputStream();// 负责接收消息
            //字节转字符的过程
            //文件传输
//            BufferedOutputStream outputStream1 = new BufferedOutputStream(outputStream);
            writer = new BufferedWriter(new OutputStreamWriter(outputStream));
            reader = new BufferedReader(new InputStreamReader(inputStream));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            while (true) {
                String str = reader.readLine();
                System.out.println("接受到客户端的信息为 : " + str);
                if (str.equals("exit")) {
                    break;
                }
                System.out.println("请输入回复信息:");
                String s = scanner.nextLine();
                writer.write(s + "\n");
                writer.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                inputStream.close();
                outputStream.close();
                reader.close();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

我们可以从上述UDP和TCP得到两者区别:
在这里插入图片描述

若想对socket编程结构有深入了解,请看这篇博客:基于TCP/IP和UDP协议的socket编程结构解析,写的很不错,点赞!

猜你喜欢

转载自blog.csdn.net/qq_44682003/article/details/109960945