(二)通过Socket实现TCP编程

作者: 黒猫 | 来源:发表于2017-05-22 11:39 被阅读177次

    1、基础简介

    TCP协议是面向连接的、可靠的,在数据传输之前建立连接,以此保证数据的可靠性;其次,TCP协议是有序的,并且以字节流的形式发送数据。在Java中基于TCP协议实现网络通讯的类有两个,分别是用于客户端的Socket类及用于服务器端的ServerSocket类,Socket通讯模型如下:

    两台主机若想实现通讯,就必然存在着一台作为服务器端(Server),一台作为客户端(Client),首先在服务器端建立一个ServerSocket,并且绑定相应的端口进行监听,等待客户端的连接;之后在客户端创建Socket并向服务器端发送请求,此时服务器端收到客户端发送的请求,并创建连接Socket用于与客户端的Socket进行通信;通信的过程就是借助InputStream以及OutputStream实现数据的发送、接收、响应等等,按照相关的协议对Socket进行读与写的操作,在通信结束后需要关闭双方的Socket及相关资源,即输入与输出流等,以此断开通信,以上便是基于TCP协议的Socket通讯进行的整个过程。


    2、通过编程实现“用户登录功能”之客户端

    实现用户登录,实际上就是用户信息,例如用户名及密码,从客户端向服务器端发送,被服务器端接收后,再向客户端进行响应,例如回复欢迎登陆等信息的过程。

    创建客户端的具体步骤:

    1. 创建Socket对象,指明需要连接的服务器地址和端口号
    2. 建立连接后,通过输出流向服务器端发送请求信息
    3. 通过输入流获取服务器端响应的信息
    4. 关闭相关资源

    示例代码如下:

    import java.io.IOException;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.Socket;
    import java.net.UnknownHostException;
    
    public class Client {
    
        public static void main(String[] args) {
            try {
                /*
                 * 创建客户端Socket,指定服务器地址和端口 
                 * 可以理解为客户端与服务器端建立一条通道
                 * 该通道就是Socket流,也就是客户端对象
                 * Socket流中既有字节输入流,也有字节输出流
                 * 本案例中,服务器端和客户端都在本地同一台主机中
                 * 因此服务器地址可填“127.0.0.1”或“localhost” 
                 * 此时会出现异常,使用try-catch块捕获该异常
                 */
                Socket socket = new Socket("localhost", 2333);
                // 获取输出流,向服务器端发送信息
                OutputStream os = socket.getOutputStream();
                // 将字节输出流包装为打印流
                PrintWriter pw = new PrintWriter(os);
                // 编写要向服务器端发送的信息
                pw.write("用户名:admin;密码:123456");
                // 通过刷新实现发送
                pw.flush();
                // 禁用输出流
                socket.shutdownOutput();
                // 关闭相关资源
                pw.close();
                os.close();
                socket.close();
            } catch (UnknownHostException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    }
    
    
    

    3、通过编程实现“用户登录功能”之服务器端

    创建服务器端的具体步骤:

    1. 创建ServerSocket对象,并绑定监听端口
    2. 调用accept()方法进行监听,等待客户端的连接请求
    3. 与客户端建立连接后,通过输入流读取客户端发送的请求信息
    4. 通过输出流向客户端发送响应信息
    5. 关闭相关资源

    示例代码如下:

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class Server {
    
        public static void main(String[] args) {
            try {
                /*
                 * 创建服务器端Socket,即ServerSocket,并绑定监听端口 
                 * 指定端口时,默认指定1023之后的端口号
                 * 此时会出现异常,使用try-catch块捕获该异常
                 */
                ServerSocket serverSocket = new ServerSocket(2333);
                /*
                 * 调用accept()方法进行监听,等待客户端的连接
                 * 实际上就是获取之前创建的客户端Socket对象
                 * 这就保证了服务器端与客户端使用的是同一个Socket流
                 * 为了便于查看结果,添加一句输出
                 */
                System.out.println("****服务期即将启动,正在等待客户端连接****");
                // 一旦调用该方法,服务器端就会进入阻塞状态,等待客户端连接
                Socket socket = serverSocket.accept();
                // 获取输入流,读取客户端信息
                InputStream is = socket.getInputStream();
                // 将字节输入流转换为字符输入流
                InputStreamReader isr = new InputStreamReader(is);
                // 为字符输入流添加缓冲
                BufferedReader br = new BufferedReader(isr);
                String info = null;
                // 循环读取客户端信息
                while ((info = br.readLine()) != null) {
                    System.out.println("服务器端——读取到客户端提交如下信息:" + info);
                }
                // 将输入流置于末尾
                socket.shutdownInput();
                // 关闭相关资源
                br.close();
                is.close();
                socket.close();
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    
    }
    
    

    此时就可以启动这两段代码来检验是否能够实现“用户登录”这一功能,要注意的是必须首先启动服务器端,结果如下:

    之后在运行客户端代码,服务器端显示结果如下:


    可以看到服务器端成功接收了来自客户端的登录请求。

    注意:

    1. 关于shutdownInput()方法及shutdownOutput()方法的理解如下:编写程序的大多数的时候是可以直接使用Socket类或输入输出流的close()方法关闭网络连接,但有时会出现希望只关闭输入输出流,并不关闭网络连接的情况。这就需要用到Socket类的shutdownInput()方法及shutdownOutput()方法,这两个方法只会关闭相应的输入、输出流,而并不会同时关闭网络连接,一般只有在确定不再需要网络连接的时候,才会使用Socket类或输入输出流的close()方法关闭相关资源。
    2. 关于打印流的相关内容可以到Java IO流查看;
      关于端口号的相关内容可以到TCP/IP四层模型中第四部分“传输层”查看。

    4、完善“用户登录功能”之实现服务器端的响应

    之前仅仅实现了客户端向服务器端提交“用户信息”,但服务器端并没有向客户端发送反馈信息,进行响应,因此可在服务器端添加输出流,在客户端添加输入流,修改后的服务器端示例代码如下:

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class Server {
    
        public static void main(String[] args) {
            try {
                /*
                 * 创建服务器端Socket,即ServerSocket,并绑定监听端口 
                 * 指定端口时,默认指定1023之后的端口号
                 * 此时会出现异常,使用try-catch块捕获该异常
                 */
                ServerSocket serverSocket = new ServerSocket(2333);
                /*
                 * 调用accept()方法进行监听,等待客户端的连接 
                 * 为了便于查看结果,添加一句输出
                 */
                System.out.println("****服务期即将启动,正在等待客户端连接****");
                // 一旦调用该方法,服务器端就会进入阻塞状态,等待客户端连接
                Socket socket = serverSocket.accept();
                // 获取输入流,读取客户端信息
                InputStream is = socket.getInputStream();
                // 将字节输入流转换为字符输入流
                InputStreamReader isr = new InputStreamReader(is);
                // 为字符输入流添加缓冲
                BufferedReader br = new BufferedReader(isr);
                String info = null;
                // 循环读取客户端信息
                while ((info = br.readLine()) != null) {
                    System.out.println("服务器端——读取到客户端提交如下信息:" + info);
                }
                // 将输入流置于末尾
                socket.shutdownInput();
    
                // 获取输出流,响应客户端的请求
                OutputStream os = socket.getOutputStream();
                // 将字节输出流包装为打印流
                PrintWriter pw = new PrintWriter(os);
                // 编写服务器端的响应信息
                pw.write("欢迎登录!");
                // 通过刷新实现发送
                pw.flush();
    
                // 关闭相关资源
                pw.close();
                os.close();
                br.close();
                is.close();
                socket.close();
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    
    }
    
    

    修改后的客户端示例代码如下:

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.Socket;
    import java.net.UnknownHostException;
    
    public class Client {
    
        public static void main(String[] args) {
            try {
                /*
                 * 创建客户端Socket,指定服务器地址和端口 
                 * 本案例中,服务器端和客户端都在本地同一台主机中
                 * 因此服务器地址可填“127.0.0.1”或“localhost” 
                 * 此时会出现异常,使用try-catch块捕获该异常
                 */
                Socket socket = new Socket("localhost", 2333);
                // 获取输出流,向服务器端发送信息
                OutputStream os = socket.getOutputStream();
                // 将字节输出流包装为打印流
                PrintWriter pw = new PrintWriter(os);
                // 编写要向服务器端发送的信息
                pw.write("用户名:admin;密码:123456");
                // 通过刷新实现发送
                pw.flush();
                // 禁用输出流
                socket.shutdownOutput();
    
                // 获取输入流,读取服务器端的响应信息
                InputStream is = socket.getInputStream();
                // 将字节输入流转换为字符输入流
                InputStreamReader isr = new InputStreamReader(is);
                // 为字符输入流添加缓冲
                BufferedReader br = new BufferedReader(isr);
                String info = null;
                // 循环读取客户端信息
                while ((info = br.readLine()) != null) {
                    System.out.println("客户端——读取到服务器端反馈如下信息:" + info);
                }
    
                // 关闭相关资源
                br.close();
                is.close();
                pw.close();
                os.close();
                socket.close();
            } catch (UnknownHostException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    }
    
    

    此时启动服务器端,结果如下:

    之后在运行客户端代码,服务器端显示结果如下:


    此时客户端结果如下:


    可以看到客户端成功接收了来自服务器端的响应信息。


    5、使用多线程实现服务器与多客户端进行通信

    之前的案例仅仅实现了一台客户端与服务器进行通信的过程,但在实际生活中,往往是在服务器上运行一个永久的程序,而且可以与多个客户端进行通信,并且可以接收多个客户端的请求,提供相应的服务。
      关于多线程的相关内容可以到已完结的深入浅出Java多线程专题查看。

    使用多线程实现的具体步骤:

    1. 创建服务器端ServerSocket对象,循环调用accept()方法等待客户端的连接
    2. 客户端创建socket并请求与服务器端连接
    3. 服务器端接受客户端的请求,创建socket与该客户端建立专线连接
    4. 建立专线连接的服务器端socket与客户端socket在一个单独的线程上对话
    5. 之后服务器端继续等待新的客户端连接

    首先创建单独的线程类,用来处理客户端的请求,示例代码如下:

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.Socket;
    
    /*
     * 此类为服务器线程处理类
     * 继承Thread类
     * 每当有客户端发送请求
     * 服务器端都会创建一个Socket与之通信
     */
    public class ServerThread extends Thread {
        // 创建与本线程相关的Socket
        Socket socket = null;
        // 使用构造方法初始化Socket
        public ServerThread(Socket socket) {
            this.socket = socket;
        }
        // 线程执行操作,响应客户端的请求,重写父类的run()方法
        public void run() {
            // 初始化输入输出流
            InputStream is = null;
            BufferedReader br = null;
            OutputStream os = null;
            PrintWriter pw = null;
            try {
                is = socket.getInputStream();
                // 将字节输入流转换为字符输入流
                InputStreamReader isr = new InputStreamReader(is);
                br = new BufferedReader(isr);
                String info = null;
                // 循环读取客户端信息
                while ((info = br.readLine()) != null) {
                    System.out.println("服务器端——读取到客户端提交如下信息:" + info);
                }
                // 将输入流置于末尾
                socket.shutdownInput();
                os = socket.getOutputStream();
                pw = new PrintWriter(os);
                // 编写服务器端的响应信息
                pw.write("欢迎登录!");
                // 通过刷新实现发送
                pw.flush();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                /*
                 * 为了保证相关资源一定能够关闭 将其放置在finally代码块中 
                 * 并增加if判断条件验证是否为空,避免报错
                 * 最后再统一用try-catch块包围处理异常
                 */
                try {
                    if (pw != null) {
                        pw.close();
                    }
                    if (os != null) {
                        os.close();
                    }
                    if (br != null) {
                        br.close();
                    }
                    if (is != null) {
                        is.close();
                    }
                    if (socket != null) {
                        socket.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
        }
    
    }
    
    

    之后创建服务器端,示例代码如下:

    import java.io.IOException;
    import java.net.InetAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class Server {
    
        public static void main(String[] args) {
            try {
                /*
                 * 创建服务器端Socket,即ServerSocket,并绑定监听端口 
                 * 指定端口时,默认指定1023之后的端口号
                 * 此时会出现异常,使用try-catch块捕获该异常
                 */
                ServerSocket serverSocket = new ServerSocket(2333);
                // 为了便于查看结果,添加一句输出
                System.out.println("****服务期即将启动,正在等待客户端连接****");
                // 初始化Socket
                Socket socket = null;
                // 记录连接过的客户端数量
                int count = 0;
                // 循环监听等待客户端的连接
                while (true) {
                    /*
                     * 调用accept()方法进行监听,一旦调用该方法 
                     * 服务器端就会进入阻塞状态,等待客户端连接
                     */
                    socket = serverSocket.accept();
                    // 创建一个新的线程
                    ServerThread serverThread = new ServerThread(socket);
                    /*
                     * 设置线程优先级,范围是[1,10],默认是5
                     * 未设置线程优先级可能会导致运行时速度非常慢
                     * 可降低优先级
                     */
                    serverThread.setPriority(4);                
                    // 启动线程
                    serverThread.start();
                    // 统计连接过的客户端的数量
                    count++;
                    System.out.println("已累计服务" + count + "台客户端!");
                    // 获取连接的客户端的IP地址
                    InetAddress address = socket.getInetAddress();
                    System.out.println("当前客户端的IP地址是:" + address.getHostAddress());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    
    }
    
    

    最后创建客户端,示例代码如下:

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.Socket;
    import java.net.UnknownHostException;
    
    public class Client {
    
        public static void main(String[] args) {
            try {
                /*
                 * 创建客户端Socket,指定服务器地址和端口 
                 * 本案例中,服务器端和客户端都在本地同一台主机中
                 * 因此服务器地址可填“127.0.0.1”或“localhost” 
                 * 此时会出现异常,使用try-catch块捕获该异常
                 */
                Socket socket = new Socket("localhost", 2333);
                // 获取输出流,向服务器端发送信息
                OutputStream os = socket.getOutputStream();
                // 将字节输出流包装为打印流
                PrintWriter pw = new PrintWriter(os);
                // 编写要向服务器端发送的信息
                pw.write("用户名:admin;密码:123456");
                // 通过刷新实现发送
                pw.flush();
                // 禁用输出流
                socket.shutdownOutput();
                // 获取输入流,读取服务器端的响应信息
                InputStream is = socket.getInputStream();
                // 将字节输入流转换为字符输入流
                InputStreamReader isr = new InputStreamReader(is);
                // 为字符输入流添加缓冲
                BufferedReader br = new BufferedReader(isr);
                String info = null;
                // 循环读取客户端信息
                while ((info = br.readLine()) != null) {
                    System.out.println("客户端——读取到服务器端反馈如下信息:" + info);
                }
                // 关闭相关资源
                br.close();
                is.close();
                pw.close();
                os.close();
                socket.close();
            } catch (UnknownHostException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    }
    
    

    此时启动服务器端,结果如下:

    之后在运行客户端代码,服务器端显示结果如下:


    之后修改客户端的用户名为“Tom”,密码为“654321”,模拟多客户端登录,重新运行客户端代码,服务器端结果如下:


    可见成功实现了服务器端与多个客户端进行通信。


    版权声明:欢迎转载,欢迎扩散,但转载时请标明作者以及原文出处,谢谢合作!             ↓↓↓
    

    相关文章

      网友评论

        本文标题:(二)通过Socket实现TCP编程

        本文链接:https://www.haomeiwen.com/subject/fllstxtx.html