Java【网络编程2】使用 TCP 的 Socket API 实现客户端服务器通信(保姆级教学, 附代码)


前言

📕各位读者好, 我是小陈, 这是我的个人主页
📗小陈还在持续努力学习编程, 努力通过博客输出所学知识
📘如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽
📙 希望我的专栏能够帮助到你:
JavaSE基础: 基础语法, 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统等
Java数据结构: 顺序表, 链表, 堆, 二叉树, 二叉搜索树, 哈希表等
JavaEE初阶: 多线程, 网络编程, TCP/IP协议, HTTP协议, Tomcat, Servlet, Linux, JVM等(正在持续更新)

上篇文章介绍了基于 UDP 协议的 Socket API, 以及简单写了一个回显服务器, 实现了服务器和客户端之间的网络通信

本篇将介绍网络编程中 : 基于 TCP 协议的 Socket 套接字的相关知识


提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!

一、认识 Socket(套接字), TCP 协议和 UDP 协议

以下内容上篇介绍过了, 看过上篇文章的读者可以跳过

上篇提到, 我们程序员进行网络编程主要是在 TCP/IP 五层网络模型中的应用层, 而数据在网络上传输, 需要进行封装和分用, 其中应用层需要调用传输层提供的 API , 这一组 API 就被称作 Socket API

1, 什么是 Socket(套接字)

概念 : Socket 套接字是由系统提供于网络通信的技术, 是基于 TCP/IP 协议的网络通信的基本操作党员, 基于 Socket 套接字的网络程序开发就是网络编程

要进行网络通信, 需要有一个 socket 对象, 一个 socket 对象对应着一个 socket 文件, 这个文件在 网卡上而不是硬盘上, 所以有了 sokcet 对象才能通过操作内存来操作网卡
在 socket 文件中写数据相当于通过网卡发送数据, 在 socket 文件中读数据相当于通过网卡接收数据

Socket API 分为两类 : 基于 TCP 协议的 API , 和基于 UDP 协议的 API, 下面先认识一下 TCP 协议和 UDP 协议的区别和特点


2, 浅谈 TCP 协议和 UDP 协议的区别和特点

TCP 协议说明UDP 协议说明
有连接通信双方需要刻意保存对方的相关信息无链接通信双方不需要刻意保存对方的信息
可靠传输如果数据发送不成功, 发送方会知道不可靠传输发送方不关心数据是否发送成功
面向字节流发送的数据以字节为单位面向数据报发送的数据以 UDP 数据报为单位
全双工双向通信全双工双向通信

这里只做简单介绍, 这两个协议后续会单独详细介绍


二、基于 TCP 协议的 Socket API

首先要明确 TCP 协议和 UDP 协议的很重要的区别 : TCP 协议是有链接, 面向字节流传输, 主要体现在 : 发送方和接收方在网络通信之间要先建立连接, 并且传输的数据的基本单位是字节

基于 TCP 协议的 Socket API 中, 要分清楚以下两个类 :

类名解释
ServerSocket只能服务器使用, 客户端不能使用, 这个类是在等待客户端发起连接之前不做任何事的"监听器"
Socket服务器或客户端都可以使用, 客户端使用这个类向服务器发起连接之后, 双端都使用这个类进行网络通信

这两个类的联系就是, 服务器启动之后, 先使用 ServerSocket 类等待客户端发来连接请求, 连接成功后服务器和客户端都使用 Socket 类进行通信


1, ServerSocket 类

ServerSocket 类的构造方法 :

方法签名作用
ServerSocket (int port)创建一个 ServerSocket 对象, 一般用于服务器, 需要指定本机端口号

ServerSocket 类的成员方法 :

方法签名作用
Socket accept()开始"监听", 有客户端发来连接请求之后, 返回一个用于服务器使用的 Socket 对象, 如果客户端没有发起连接, 则阻塞等待
void close()关闭 ServerSocket

2, Socket 类

再次说明, Socket 这个类用于客户端, 也可以在服务器与客户端连接之后使用, 无论客户端或服务器使用, 都会保存对端的相关信息

Socket 类的构造方法 :

方法签名作用
Socket(String host, int port)一般用于客户端, 需要指定服务器的 IP 地址和端口号
void close()用于关闭 ServerSocket

Socket 类的成员方法 :
由于 TCP 协议是面向字节流, 所以有两个关于字节流输入输出的成员方法

方法签名作用
InputStream getInputStream()获取 Socket 的字节输入流
OutputStream getOutputStream()获取 Socket 的字节输出流
InetAddress getInetAddress()获取对端的 IP 地址
InetAddress getPort()获取对端的端口号

调用 getInputStream() 和 getOutputStream() 这个两个方法, 就可以通过字节流对象, 从网卡中读写数据
getInputStream()返回的对象用来输入(读), 从网卡读数据到内存(从网卡接收数据)
getOutputStream返回的对象用来输出(写), 从网卡写数据到内存(从网卡发送数据)

先对上述 API 有个印象即可, 接下来逐行解析如何从 0 到 1 地进行客户端和服务器之间地网络编程, 代码敲完之后再消化吸收


三、逐行代码解析网络编程

下面我们还是写一个最简单的客户端服务器网络通信模型 : 客户端给服务器发送什么请求, 服务器就给客户发送什么响应(这是最简单但是毫无意义的回显服务器, 只是方便熟悉 TCP Socket 的 API 使用)

客户端和服务器各自为一个进程在运行, 双方互不干涉(当然我们现在要写的客户端服务器程序是在同一台主机上的)

一定是服务器先启动, 一直等待客户端发来请求, 所以按照时间顺序, 代码逻辑应该如下所示 :

客户端服务器
/1, 启动服务器, 构造 ServerSocket 对象, 调用 accept() 时刻准备和客户端连接
2, 构造 Socket 对象即为发起连接/
/3, 连接成功, 通过 accept() 的返回值得到 Socket 对象
4, 把请求写入网卡
/5, 从网卡读取请求
/6, 处理请求
/7, 把响应写入网卡
8, 从网卡读取响应/

有了这个思路, 下面正式开始使用上述 API 进行网络编程


1, 逐行解析客户端

创建一个类 : TCPEchoClient 作为客户端

成员属性 :
需要定义一个 Scoket 对象来进行和服务器的通信

public class TCPEchoClient {
	// 成员属性
    private Socket socket = null;
}

构造方法 :
用于实例化客户端的 socket 对象, 别忘了需要绑定服务器的 IP 地址和端口号

public class TCPEchoClient {
	// 成员属性
    private Socket socket = null;
    
    // 构造方法
	public TCPEchoClient(String serverIP, int serverPort) throws IOException {
        socket = new Socket(serverIP, serverPort);
    }
}

main 方法 :
1, 构造 tcpEchoClient 对象, 由于服务器在本机, IP 地址为"127.0.0.1", 端口号随意指定 [1024, 65535] 之间的任意一个
2, 调用 TCPEchoClient 类的核心成员方法 start(), 这个方法实现了客户端的核心逻辑

public class TCPEchoClient {
	// 成员属性
    private Socket socket = null;
    
    // 构造方法
	public TCPEchoClient(String serverIP, int serverPort) throws IOException {
        socket = new Socket(serverIP, serverPort);
    }
    
	// main 方法
    public static void main(String[] args) throws IOException {
        TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9999);
        tcpEchoClient.start();
    }
}

1.1, 核心成员方法 start()

1️⃣构造一个 Scanner 对象, 从控制台输入字符串, 这个字符串当作请求的内容
2️⃣核心逻辑在一个 while(true) 循环中, 实现多次发送请求

public void start() {
        Scanner in = new Scanner(System.in);
        // 发送多个请求
        while (true) {
                
        }
    }
}

由于TCP协议是面向字节流传输, 所以为了方便读写数据, 我们把字节流转化成字符流处理

所以在进入 while 循环之前, 先构造字符流的输入输出对象

public void start() {
        Scanner in = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            // 把字节流转换成字符流
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner inFromSocket = new Scanner(inputStream);

            // 发送多个请求
            while (true) {
            
            }
       } catch (IOException e) {
            e.printStackTrace();
       }
}

getInputStream()返回的对象用来输入(读), 从网卡读数据到内存(从网卡接收数据)
getOutputStream返回的对象用来输出(写), 从网卡写数据到内存(从网卡发送数据)

然后每次进入循环, 主要有两个操作 : 1, 把请求写入网卡 2, 把响应从网卡中读出来, 写的使用调用 println(), 读的时候调用 next(), 这样能以空白符为结束标志进行读写数据

图解如下 :
在这里插入图片描述


2, 逐行解析服务器

创建一个类 TCPEchoServer 作为服务器

成员属性 :
需要定义一个 ServerSocket 对象, 用来等待客户端发来连接的"监听器"

public class TCPEchoServer {
	// 构造方法
    private ServerSocket serverSocket = null;
}

构造方法 :
用于实例化客户端的 ServerSocket 对象, 别忘了需要绑定本机端口号

public class TCPEchoServer {
	// 构造方法
    private ServerSocket serverSocket = null;
    
	// 构造方法
    public TCPEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
}

main 方法 :
1, 构造 tcpEchoServer 对象, 需要绑定端口号, 必须和客户端那边绑定的一致
2, 调用 tcpEchoServer 类的核心成员方法 start(), 这个方法实现了服务器的核心逻辑

public class TCPEchoServer {
	// 构造方法
    private ServerSocket serverSocket = null;
    
	// 构造方法
    public TCPEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    
	// main 方法
    public static void main(String[] args) throws IOException {
        TCPEchoServer tcpEchoServer = new TCPEchoServer(9999);
        tcpEchoServer.start();
    }
}

2.1, 核心成员方法 start()

由于 TCP 是有连接的传输协议, 所以服务器在和客户端连接之前, 要先和客户端建立连接, 也就是调用 accept(), 连接成功之后, 服务器就可以处理这个连接了

如果有多个服务器来和客户端连接, 服务器就需要处理多个连接, 所以把上述过程写在 while(true) 中

    public void start() throws IOException {
        while (true) {
            // 建立连接 返回一个 Socket 对象
            Socket socket = serverSocket.accept();

            // 处理连接到的这个客户端
            processConnection(socket);
        }
    }

处理连接的过程其实就是从网卡中读取数据, 处理响应, 再把响应写回网卡, 我们把这个过程封装成 processConnection(Socket socket);

注意 : 调用 accept() 的是 ServerSocket 的对象, 而这个方法的返回值是Socket, 上面已经强调过了

接下来解析 processConnection() 的过程

和客户端一样, 要先把字节流转化成字符流, 方便读写数据

	private void processConnection(Socket socket) {
        System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端上线");
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream() ) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner inFromSocket = new Scanner(inputStream);

            // 处理多个请求
            while(true) {
            
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

getInputStream()返回的对象用来输入(读), 从网卡读数据到内存(从网卡接收数据)
getOutputStream返回的对象用来输出(写), 从网卡写数据到内存(从网卡发送数据)

然后就是while循环, 进入循环后主要就三个操作: 1, 从网卡中读取数据 2, 处理响应 3, 再把响应写回网卡

图解如下 :
在这里插入图片描述

看到这里, 应该感受到了 TCP 和 UDP 的不同之处体现在哪了
首先是 TCP 的服务器需要先使用 ServerSocket 建立连接, 建立连接之后服务器和客户端都是用 Socket 进行通信
通信时, TCP 进行传输使用的是字节流, 直接从网卡读写, 但我们可以转化成字符流, 而 UDP 进行传输是把数据封装成 DatagramPacket(数据报), 再进行发送和接收


3, bug 修改

3.1, bug1

上述代码中, 有个隐性的严重的 bug, 由于服务器可能是处理多个客户端连接, 那么处理完客户端 A 后, 服务器这个进程不一定会结束, 很有可能还要处理客户端 B

所以服务器和某个客户端进行通信时打开的 Socket 文件就必须在 finally 语句块中调用 close(), 以避免内存资源泄露, 修改后的代码如下 :

    private void processConnection(Socket socket) throws IOException {
        System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端上线");
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream() ) {
            // 处理多个请求
            while(true) {
                
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            socket.close();
        }
    }

客户端那边不需要调用 close() 是因为在当前场景下, 客户端的 Socket 生命争取伴随着整个客户端进程, 不会出现频繁创建 Socket 但没有 close 导致内存资源泄露


3.2, bug2

还有一个显性的 bug, 我们首先打开两个客户端, 步骤如下 :
在这里插入图片描述
在这里插入图片描述

然后先运行服务器, 再运行两个客户端, 观察运行效果 :
1 号客户端 :
在这里插入图片描述
2 号客户端 :
在这里插入图片描述
服务器 :
在这里插入图片描述

会发现, 第二个开启的客户端并没有和服务器成功通信, 这是因为, 我们的服务器处理多个连接时, 是在一个while循环中, 如果第一个连接的客户端没有下线, 就不会接收第二个客户端的连接

    public void start() throws IOException {
        while (true) {
            // 建立连接 返回一个 Socket 对象
            Socket socket = serverSocket.accept();

            // 处理连接到的这个客户端
            processConnection(socket);
        }
    }

正确的代码应该是, 每连接成功一个客户端, 就开启一个线程来处理这个连接, 修改后的代码如下 :

    public void start() throws IOException {
        while (true) {
            // 建立连接 返回一个 Socket 对象
            Socket socket = serverSocket.accept();

            // 处理连接到的这个客户端
            Thread thread = new Thread( () -> {
                try {
                    processConnection(socket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            // 别忘了调用 start() 启动线程
            thread.start();
        }
    }

3.3, 最终运行效果

1 号客户端 :
在这里插入图片描述

2 号客户端 :
在这里插入图片描述

服务器 :
在这里插入图片描述


四、完整代码

1, 客户端

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 {
        socket = new Socket(serverIP, serverPort);
    }

    public void start() {
        Scanner in = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            // 把字节流转换成字符流
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner inFromSocket = new Scanner(inputStream);

            // 发送多个请求
            while (true) {
                // 1,从控制台输入字符串
                String requestString = in.next();

                // 2,写入请求
                printWriter.println(requestString);
                printWriter.flush();

                // 3,读取请求
                String responseString = inFromSocket.next();

                // 控制台 打印请求字符串 + 响应字符串
                System.out.println(requestString + " + " + responseString);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9999);
        tcpEchoClient.start();
    }
}

2, 服务器

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TCPEchoServer {
    private ServerSocket serverSocket = null;

    public TCPEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        while (true) {
            // 建立连接 返回一个 Socket 对象
            Socket socket = serverSocket.accept();

            // 处理连接到的这个客户端
            Thread thread = new Thread( () -> {
                try {
                    processConnection(socket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            // 别忘了调用 start() 启动线程
            thread.start();
        }
    }

    private void processConnection(Socket socket) throws IOException {
        System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端上线");
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream() ) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner inFromSocket = new Scanner(inputStream);

            // 处理多个请求
            while(true) {
                if (!inFromSocket.hasNext()) {
                    System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端下线");
                    break;
                }

                // 1,读取请求
                String requestString = inFromSocket.next();

                // 2,处理请求
                String responseString = process(requestString);

                // 3,写入响应
                printWriter.println(responseString);
                printWriter.flush();

                // 控制台打印 客户端IP地址 + 客户端端口号 + 请求字符串 + 响应字符串
                System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + requestString + " + " + responseString);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            socket.close();
        }
    }

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

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

总结

以上就是本篇的全部内容, 主要介绍了 : 基于 TCP协议的 Socket API , 以及利用这些 API 写了一个最简单但无意义的客户端服务器网络通信程序

再回顾一下, Socket 类的成员方法 :
由于TCP协议是面向字节流, 所以有两个成员方法是关于字节流输入输出的

方法签名作用
InputStream getInputStream()获取 Socket 的字节输入流
OutputStream getOutputStream()获取 Socket 的字节输出流
InetAddress getInetAddress()获取对端的 IP 地址
InetAddress getPort()获取对端的端口号

如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~


上山总比下山辛苦
下篇文章见

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灵魂相契的树

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值