Java SE | 网络编程 TCP、UDP协议 Socket套接字的使用

网络编程

网络协议

  • 如同人与人之间相互交流是需要遵循一定的规则(如语言)一样,计算机之间能够进行相互通信是因为它们都共同遵守一定的规则,即网络协议。OSI参考模型和TCP/IP模型在不同的层次中有许多不同的网络协议,如图所示:
    在这里插入图片描述

  • 网络协议之间的关系图如下:
      在这里插入图片描述

  • IP协议(Internet protocol)

    IP协议的作用在于把各种数据包准备无误的传递给对方,其中两个重要的条件是IP地址和MAC地址。由于IP地址是稀有资源,不可能每个人都拥有一个IP地址,所以我们通常的IP地址是路由器给我们生成的IP地址,路由器里面会记录我们的MAC地址。而MAC地址是全球唯一的。举例,IP地址就如同是我们居住小区的地址,而MAC地址就是我们住的那栋楼那个房间那个人。IP地址采用的IPv4格式,目前正在向IPv6过渡。

  • TCP协议(Transmission Control Protocol)

    TCP(传输控制协议)是面向连接的传输层协议。TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。TCP协议采用字节流传输数据。

  • 三次握手与四次挥手
    TCP是面向连接的协议,因此每个TCP连接都有3个阶段:连接建立、数据传送和连接释放。连接建立经历三个步骤,通常称为“三次握手”。

  • TCP三次握手过程如下:在这里插入图片描述

  1. 第一次握手(客户端发送请求)
    客户机发送连接请求报文段到服务器,并进入SYN_SENT状态,等待服务器确认。发送连接请求报文段内容:SYN=1,seq=x;SYN=1意思是一个TCP的SYN标志位置为1的包,指明客户端打算连接的服务器的端口;seq=x表示客户端初始序号x,保存在包头的序列号(Sequence Number)字段里。
  2. 第二次握手(服务端回传确认)
    服务器收到客户端连接请求报文,如果同意建立连接,向客户机发回确认报文段(ACK)应答,并为该TCP连接分配TCP缓存和变量。服务器发回确认报文段内容:SYN=1,ACK=1,seq=y,ack=x+1;SYN标志位和ACK标志位均为1,同时将确认序号(Acknowledgement Number)设置为客户的ISN加1,即x+1;seq=y为服务端初始序号y。
  3. 第三次握手(客户端回传确认)
    客户机收到服务器的确认报文段后,向服务器给出确认报文段(ACK),并且也要给该连接分配缓存和变量。此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。客户端发回确认报文段内容:ACK=1,seq=x+1,ack=y+1;ACK=1为确认报文段;seq=x+1为客户端序号加1;ack=y+1,为服务器发来的ACK的初始序号字段+1。
  • TCP四次挥手过程如下:

在这里插入图片描述
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

  1. TCP客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入FIN_WAIT_1状态。发送报文段内容:FIN=1,seq=u;FIN=1表示请求切断连接;seq=u为客户端请求初始序号。
  2. 服务端收到这个FIN,它发回一个ACK给客户端,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号;服务端进入CLOSE_WAIT状态。发送报文段内容:ACK=1,seq=v,ack=u+1;ACK=1为确认报文;seq=v为服务器确认初始序号;ack=u+1为客户端初始序号加1。
  3. 服务器关闭客户端的连接后,发送一个FIN给客户端,服务端进入LAST_ACK状态。发送报文段内容:FIN=1,ACK=1,seq=w,ack=u+1;FIN=1为请求切断连接,ACK=1为确认报文,seq=w为服务端请求切断初始序号。
  4. 客户端收到FIN后,客户端进入TIME_WAIT状态,接着发回一个ACK报文给服务端确认,并将确认序号设置为收到序号加1,服务端进入CLOSED状态,完成四次挥手。发送报文内容:ACK=1,seq=u+1,ack=w+1;ACK=1为确认报文,seq=u+1为客户端初始序号加1,ack=w+1为服务器初始序号加1。
  • 注意:为什么连接的时候是三次握手,关闭的时候却是四次挥手?

因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭socket,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文,我收到了”。只有等到服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送,故需要四步挥手。

UDP协议(User Datagram Protocol)

  • UDP,用户数据报协议,它是TCP/IP协议簇中无连接的运输层协议。
  1. UDP是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
  2. 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务器可同时向多个客户端传输相同的消息。
  3. UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。
  4. 吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制。
  5. UDP使用尽量最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态表。
  6. UDP是面向报文的。发送方的UDP对应用程序交下来的报文,在添加首部受就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。

UDP协议格式

在这里插入图片描述
UDP协议由两部分组成:首部和数据。其中,首部仅有8个字节,包括源端口和目的端口、长度(UDP用于数据报的长度)、校验。

TCP与UDP的区别

  1. TCP基于连接,UDP是无连接的;
  2. 对系统资源的要求,TCP较多,UDP较少;
  3. UDP程序结构较简单;
  4. TCP是流模式,而UDP是数据报模式;
  5. TCP保证数据正确性,而UDP可能丢包;TCP保证数据顺序,而UDP不保证;

Socket概述

  • Java的网络编程主要涉及到的内容是Socket编程。Socket套接字,就是两台主机之间逻辑连接的端点。TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。Socket是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。

  • 应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。-

  • Socket,实际上是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。实际上,Socket跟TCP/IP协议没有必然的关系,Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现,只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、listen、accept、send、read和write等等。网络有一段关于socket和TCP/IP协议关系的说法比较容易理解:

  • “TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。这个就像操作系统会提供标准的编程接口,比如win32编程接口一样,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。”

  • 实际上,传输层的TCP是基于网络层的IP协议的,而应用层的HTTP协议又是基于传输层的TCP协议的,而Socket本身不算是协议,就像上面所说,它只是提供了一个针对TCP或者UDP编程的接口。socket是对端口通信开发的工具,它要更底层一些。

Socket整体流程

  • Socket编程主要涉及到客户端和服务端两个方面,首先是在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536,但是0到1024是为特权服务保留的端口号,我们可以选择任意一个当前没有被其他进程使用的端口。
  • 客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。

在这里插入图片描述

示例

TCP协议通信

  • 服务器端 Server
public class Server {
    public static void main(String[] args) throws Exception {
        int port = 8080;
        int clientNo = 1;
        ServerSocket server = new ServerSocket(port);
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while (true) {
            Socket socket = server.accept();
            threadPool.execute(new SingleServer(socket,clientNo));
            clientNo++;
        }
    }
}
class SingleServer implements Runnable {
    public Socket socket;
    private int clientNo;
    public SingleServer(Socket socket, int clientNo) {
        this.socket = socket;
        this.clientNo = clientNo;
    }
    public SingleServer(int port) {
    }
    @Override
    public void run() {
        try {
            DataInputStream dis = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
            DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
            do {
                double in = dis.readDouble();
                System.out.println("输入的边长为:"+in);
                dos.writeDouble(in * in);
                dos.flush();
            } while (dis.readInt() != 0);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.println("客户端"+clientNo+"通信结束");
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 客户端 Client
public class Client {
    public static void main(String[] args) throws Exception{
        Socket socket = new Socket("127.0.0.1", 8080);
        DataInputStream dis = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
        DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("输入边长");
            double length = sc.nextDouble();
            dos.writeDouble(length);
            dos.flush();
            double area = dis.readDouble();
            System.out.println("面积:"+area);
            socket.close();
        }
    }
}

UDP协议通信

  • 发送方
public class UDPSend {
    public static void main(String[] args) throws Exception {
        String str = "abc";
        DatagramSocket datagramSocket = new DatagramSocket();
        DatagramPacket datagramPacket = new DatagramPacket(str.getBytes("utf8"), str.length(),
                InetAddress.getByName("localhost"), 9999);
        System.out.print("发送了:");
        datagramSocket.send(datagramPacket);
        System.out.println(datagramPacket);
        datagramSocket.close();
    }
}

  • 接收方
public class UDPRecv {
    public static void main(String[] args) throws Exception{
        DatagramSocket datagramSocket = new DatagramSocket(9999);
        DatagramPacket datagramPacket = new DatagramPacket(new byte[1024], 1024);
        datagramSocket.receive(datagramPacket);
        datagramSocket.close();
        byte[] data = datagramPacket.getData();
        String str = new String(data, 0, datagramPacket.getLength(),"utf8");
        System.out.println(str);
    }
}

聊天室案例

  • 服务器端
public class Server {
    public static void main(String[] args) {
        System.out.println("============服务开启============");

        System.out.println("等待用户接入");
        PrintWriter pw = null;
        Scanner input = null;
        Scanner inScanner = null;
        ServerSocket ss = null;

        try {
            // 创建Socket
            ss = new ServerSocket(9998);
            // 创建一个接收连接客户端的对象
            Socket accept = ss.accept();
            System.out.println(accept.getInetAddress()+"连接成功!");

            pw = new PrintWriter(accept.getOutputStream());
            pw.println("您已连接到服务器!");
            pw.flush();

            // 开启键盘输入
            input = new Scanner(System.in);
            inScanner = new Scanner(accept.getInputStream());

            while (inScanner.hasNextLine()) {
                String indata = inScanner.nextLine();
                System.out.println("客户端:"+indata);
                System.out.println("我(客户端):");
                String keyboarddata = input.nextLine();
                System.out.println("服务器:"+ keyboarddata);
                System.out.println("我(服务器):");
                pw.println(keyboarddata);
                pw.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 客户端
public class Client {
    public static void main(String[] args) {
        System.out.println("============连接开启============");
        Socket socket = null;
        Scanner keyboardScanner = null;
        Scanner inScanner = null;
        PrintWriter pw = null;

        try {
            socket = new Socket("localhost", 9998);
            // 读入
            inScanner = new Scanner(socket.getInputStream());
            System.out.println(inScanner.nextLine());
            pw = new PrintWriter(socket.getOutputStream());
            System.out.println("我(客户端):");
            // 先读取键盘录入方可向服务器端发送消息
            keyboardScanner = new Scanner(System.in);
            //录入键盘
            while (keyboardScanner.hasNextLine()) {
                String keyboarddata = keyboardScanner.nextLine();
                // 展示到我方控制台
                System.out.println("我(客户端):"+keyboarddata);
                pw.println(keyboarddata);
                pw.flush();
                // 阻塞等待接收服务器端的消息
                String indata = inScanner.nextLine();
                System.out.println("服务器端:"+indata);
                System.out.println("我(客户端):");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值