【java】网络编程——TCP协议通信

通信协议

根据ip可以找到主机,根据端口可以找到要给哪个进程发信息,接下来就是通过遵循相同的协议来发送信息。

本节主要讲解TCP、UDP两种协议

TCP、UDP的区别:

可以把 TCP 和 UDP 这两种协议想象成两种不同的送快递方式,下面为你详细对比它们的区别:

送快递前是否先联系

  • TCP(像靠谱的快递员):送快递前会和收件人先联系确认。比如快递员打电话问收件人 “在不在家呀,我准备送快递过来啦”,收件人回复 “在呢,你送来吧”,快递员回复“好的,我来了”。在网络里,这就是 TCP 的 “三次握手”,在传数据前先建立可靠的连接,确保对方能接收数据。就像你用浏览器打开网页,浏览器和网站服务器之间就会先通过 TCP 协议建立连接,确认没问题了才开始传网页内容。
  • UDP(像风风火火的快递员):不提前联系收件人,直接把快递放快递柜就走。它不关心收件人在不在家、能不能及时拿到快递。在网络中,UDP 协议发送数据前不会建立连接,直接就把数据发出去,简单又快速。比如在线玩游戏或者看视频直播,用 UDP 协议传输数据,能保证游戏画面和视频的流畅性,就算偶尔丢点数据,也不太影响整体体验。

送快递过程是否确保完整

  • TCP(认真负责到底):送快递过程中会确保每一个包裹都准确无误地送到收件人手里。如果发现有包裹丢了或者损坏了,会重新送一次。在网络传输里,TCP 协议会对数据进行编号,接收方收到数据后会反馈给发送方,如果发送方发现有数据没收到反馈,就会重新发送,保证数据完整、顺序正确。就像你下载一个大文件,用 TCP 协议下载,文件不会缺胳膊少腿,能完整地下载到你的电脑里。
  • UDP(主打一个速度):不太在意包裹是否完整送到。它只管尽快把包裹送出去,至于包裹有没有丢失、有没有损坏,它不会去管。在网络中,UDP 协议不会对数据进行严格的检查和重传。还是以在线游戏为例,可能偶尔会出现画面卡顿或者瞬移的情况,就是因为 UDP 协议传输时可能丢了一些数据,但为了保证速度,就不重新传了。

送快递的效率

  • TCP(稳扎稳打但慢些):因为要建立连接、确认数据完整,所以整个过程比较繁琐,花费的时间相对多一些。但它能保证数据可靠传输,适合对数据准确性要求高的场景,比如文件传输、发送邮件等。
  • UDP(快如闪电但不太稳):没有那些繁琐的步骤,直接发送数据,所以效率很高,速度很快。不过因为不太保证数据完整,所以适合对实时性要求高、对数据准确性要求相对低的场景,像语音通话、视频直播等。

TCP实现聊天

示例代码

客户端

  1. 连接服务器Socket
  2. 发送消息
package protocol;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

// Demo01的客户端
public class TcpClientDemo01 {
    public static void main(String[] args) {
        Socket socket = null;
        OutputStream os = null;
        try {
            // 1.要知道服务器的地址和端口号
            InetAddress address = InetAddress.getByName("127.0.0.1");
            int port = 9999;
            // 2.根据地址和端口创建socket连接
            socket = new Socket(address,port);
            // 3.使用io流给服务端发送信息
            os = socket.getOutputStream();
            os.write("喜欢的话给我的文章点个收藏".getBytes());

        } catch (Exception e) {
            throw new RuntimeException(e);
        }finally {
            // 关闭遵循先开后关
            // 关闭流对象
            if(os != null){
                try {
                    os.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            // 关闭套接字
            if(socket != null){
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

代码解释

  • 获取服务器地址:
    • InetAddress address = InetAddress.getByName("127.0.0.1");:通过 InetAddress.getByName 方法获取 IP 地址为 127.0.0.1(本地回环地址)的 InetAddress 对象。
    • int port = 9999;:指定服务器的端口号为 9999
  • 创建套接字连接:
    • socket = new Socket(address,port);:使用获取到的 IP 地址和端口号创建一个 Socket 对象,从而建立与服务器的 TCP 连接。
  • 发送数据:
    • os = socket.getOutputStream();:通过 socket.getOutputStream() 方法获取与该 Socket 关联的输出流。
    • os.write("喜欢的话给我的文章点个收藏".getBytes());:将字符串 “喜欢的话给我的文章点个收藏” 转换为字节数组,并通过输出流发送给服务器。

示例代码

服务端

  1. 建立服务的端口 ServerSocket
  2. 等待用户连接 accept
  3. 接收客户端的消息
package protocol;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

// Demo01的服务端
public class TcpServerDemo01 {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        Socket socket = null;
        ByteArrayOutputStream baos = null;
        try {
            // 1.建立服务端口
            serverSocket = new ServerSocket(9999);
            // 2.客户端创建连接,服务端接收连接
            socket = serverSocket.accept(); // 这里的socket其实就是客户端那里的socket对象
            // 3.读取客户端的消息,使用io流和管道流
            baos = new ByteArrayOutputStream();
            // 缓冲区
            byte[] buf = new byte[1024];
            // 传过来的信息的字节长度
            int len = 0;
            while ((len = socket.getInputStream().read(buf)) != -1) {
                // 表示流里面还有内容
                // 写到缓冲区中
                baos.write(buf, 0, len);
            }
            // 输出内容
            System.out.println(baos.toString());


        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            if(baos!=null) {
                try {
                    baos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if(socket!=null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if(serverSocket!=null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

代码解释

  • accept() 方法是一个阻塞方法,会一直等待,直到有客户端连接到该服务器。当有客户端连接时,该方法会返回一个 Socket 对象,用于与客户端进行通信。这个 Socket 对象与客户端的 Socket 对象共同构成了客户端与服务器之间的连接通道。

  • byte[] buf = new byte[1024];:创建一个长度为 1024 的字节数组 buf,作为临时缓冲区,用于存储从客户端输入流中读取的数据。

  • while ((len = socket.getInputStream().read(buf)) != -1):通过 socket.getInputStream().read(buf) 方法从客户端的输入流中读取数据到 buf 数组中,并返回实际读取的字节数 len。当 len 为 -1 时,表示已经读取到流的末尾,循环结束。

  • baos.write(buf, 0, len);:将 buf 数组中从索引 0 开始、长度为 len 的字节数据写入到 ByteArrayOutputStream 对象 baos 中。

  • 调用 baos.toString() 方法将 ByteArrayOutputStream 中存储的字节数据转换为字符串,并通过 System.out.println() 方法输出到控制台。

帮助理解

1、 为什么客户端用OutputStream,服务端用InputStream,缓冲区用ByteArrayOutputStream?

+----------------+                +----------------+                +----------------+
|    客户端       |  --(os流)-->   |    网络通道      |  --(is流)-->   |    服务端       |
|                |                |                |                |                |
|  数据产生地      |                |  数据传输路径    |                |  数据接收地     |
|                |                |                |                |                |
|  OutputStream  |                |                |                |  InputStream   |
+----------------+                +----------------+                +----------------+
                                                         |
                                                         v
                                                +------------------------+
                                                |    缓冲区               |
                                                |                        |
                                                |  临时存储区              |
                                                |                        |
                                                |  ByteArrayOutputStream |
                                                +------------------------+

2、开启的流对象,Socket,SockeServer都要关闭,关闭顺序遵循先开后关,即谁最先开启的服务谁就最后关闭。

3、先开启服务端,再开启客户端。

4、ByteArrayOutputStream 原理概述:

ByteArrayOutputStream 是 Java I/O 库中的一个类,它继承自 OutputStream。它的主要作用是将数据写入一个字节数组中,这个字节数组会随着写入数据的增加而动态扩展。你可以把它想象成一个内存中的缓冲区,程序产生的字节数据可以源源不断地写入其中,直到数据处理完毕。

ByteArrayOutputStream 内部维护了一个字节数组,用于存储写入的数据。当你调用 write 方法写入数据时,数据会被追加到这个字节数组的末尾。如果写入的数据超过了当前字节数组的容量,ByteArrayOutputStream 会自动创建一个更大的字节数组,并将原来的数据复制到新数组中,然后继续写入新的数据。

5、主要要理解客户端和服务端是怎么发送信息的(必看%%%%%%)

  • 服务端找个端口
  • 客户端根据本机ip和端口,向服务端请求连接
  • 服务端用serverSocket.accept()建立连接
  • 客户端os流向服务端发送消息
  • 服务端用is流接收并写在缓冲区

并使用 ByteArrayOutputStream 对象 baos 作为缓冲区接收数据,好处在于其能动态扩展容量以适应不定长数据、方便整合分散数据包、避免频繁内存分配与复制来提升性能、支持灵活的数据处理方式,且资源管理简单。

  • 最后输出缓冲区的数据即可

TCP实现文件上传

接收文件就用文件的管道流,接收字节数组就用字节数组的管道流(ByteArrayOutputStream),缓冲区存在于is和os之间,就相当于只要有is和os传输数据就要有一个缓冲区来充当中转站。

示例代码(客户端)

package protocol;

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

public class TcpClientDemo02 {
    public static void main(String[] args) throws Exception {

        Socket socket=new Socket(InetAddress.getByName("127.0.0.1"),9999);
        FileInputStream fis=new FileInputStream("god.jpg");
        OutputStream os = socket.getOutputStream();
        byte[] bytes = new byte[1024];
        int len;
        while ((len = fis.read(bytes)) != -1) {
            os.write(bytes, 0, len);
        }
        // 传完了通知服务端我已经结束了,但是此时并没有关闭socket
        socket.shutdownOutput(); // 然后服务端就去接收了

        // 服务端接收后发通知过来告诉客户端可以结束了,服务端传的是字节流
        InputStream is=socket.getInputStream();
        // 打印到终端,或者作为中间数据才用,若是只用存起来,直接用fos写到文件即可
        ByteArrayOutputStream bos=new ByteArrayOutputStream();
        byte[] buf = new byte[1024];
        int len1 = 0;
        while ((len1 = is.read(buf)) != -1) {
            bos.write(buf, 0, len1);
        }
        System.out.println(bos.toString());

        bos.close();
        is.close();
        os.close();
        fis.close();
        socket.close();


    }
}

代码步骤解释

  1. 创建连接:通过 Socket 类连接到服务端的指定地址和端口。
  2. 读取文件:使用 FileInputStream 打开本地文件 god.jpg
  3. 发送数据:利用 SocketOutputStream 将文件内容逐块发送给服务端。
  4. 通知结束:调用 socket.shutdownOutput() 告知服务端数据发送完毕。
  5. 接收响应:获取 SocketInputStream 接收服务端的响应,并使用 ByteArrayOutputStream 存储。
  6. 打印响应:将接收到的响应数据转换为字符串并打印。
  7. 关闭资源:依次关闭所有使用的流和 Socket 连接。

通过以上的客户端和服务端代码,实现了基于 TCP 协议的文件传输和消息交互功能。

示例代码(服务端)

package protocol;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TcpServerDemo02 {
    public static void main(String[] args) throws Exception {

        ServerSocket serverSocket=new ServerSocket(9999);
        Socket socket=serverSocket.accept();
        InputStream is=socket.getInputStream();
        FileOutputStream fos = new FileOutputStream("receive.jpg");
        byte[] b=new byte[1024];
        int len;
        while((len=is.read(b))!=-1) {
            fos.write(b, 0, len);
        }
        // 接收完了通知用户端可以关闭了
        OutputStream os=socket.getOutputStream();
        // 将String以字节流传过去
        os.write("我已经接收到了,你可以关闭了".getBytes());

        os.close();
        fos.close();
        is.close();
        socket.close();
        serverSocket.close();
    }

}

代码步骤解释

  1. 启动监听:创建 ServerSocket 并绑定到端口 9999,开始监听客户端连接。
  2. 接受连接:使用 accept() 方法等待客户端连接,连接成功后返回一个 Socket 对象。
  3. 获取输入流:通过 Socket 获取客户端的输入流,准备接收数据。
  4. 创建输出文件:使用 FileOutputStream 创建本地文件 receive.jpg,用于存储接收到的数据。
  5. 接收数据:从输入流读取数据,并将其写入本地文件。
  6. 发送通知:获取 SocketOutputStream,将通知消息以字节流形式发送给客户端。
  7. 关闭资源:依次关闭所有使用的流和 Socket 连接,最后关闭 ServerSocket

讲解出现的缓冲区

客户端出现了两次

第一次是将文件传到服务端的文件读取缓存区:是将读文件流fis---->套接字的os流

  • 作用byte[] bytes 数组作为一个缓冲区,用于从本地文件 god.jpg 中读取数据。FileInputStreamread 方法会尝试从文件中读取最多 1024 字节的数据到 bytes 数组中,并返回实际读取的字节数 len。通过使用缓冲区,避免了每次只读取一个字节的低效操作,减少了与磁盘的交互次数,提高了文件读取的效率。
  • 原理:磁盘 I/O 操作相对较慢,每次读取一个字节会频繁地进行磁盘寻道和数据传输,而使用缓冲区可以一次性读取多个字节,将多次小的磁盘 I/O 操作合并为一次较大的操作,从而提高性能。

第二次是接收服务端发来的字节流数据的数据接收缓冲区(byte[] buf)和 ByteArrayOutputStream:是将套接字的is流转换为接收字节流的bos

  • byte[] buf 的作用:作为从 Socket 输入流中读取数据的缓冲区。InputStreamread 方法会尝试从网络连接中读取最多 1024 字节的数据到 buf 数组中,并返回实际读取的字节数 len1。这样可以减少与网络的交互次数,提高数据接收的效率。
  • ByteArrayOutputStream bos 的作用ByteArrayOutputStream 是一个内存缓冲区,用于将从网络接收到的数据临时存储起来。每次从 buf 中读取到有效数据后,将其写入 bos 中。ByteArrayOutputStream 会自动管理内部的字节数组,当数据量超过当前数组容量时会自动扩容,方便将接收到的分散数据整合为一个完整的数据块。

服务端出现了一次

接收文件的文件写入缓冲区:将套接字的is流---->fos

  • 作用byte[] b 数组作为从 Socket 输入流中接收数据的缓冲区,同时也是向本地文件 receive.jpg 写入数据的中间存储区。InputStreamread 方法将从网络连接中读取的数据存储到 b 数组中,然后 FileOutputStreamwrite 方法将 b 数组中实际读取的有效数据写入到文件中。通过使用缓冲区,减少了与网络和磁盘的交互次数,提高了数据接收和文件写入的效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值