(二)通过Socket实现TCP编程


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”,模拟多客户端登录,重新运行客户端代码,服务器端结果如下:


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


版权声明:欢迎转载,欢迎扩散,但转载时请标明作者以及原文出处,谢谢合作!             ↓↓↓
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • 1、TCP状态linux查看tcp的状态命令:1)、netstat -nat 查看TCP各个状态的数量2)、lso...
    北辰青阅读 9,190评论 0 11
  • 日影渐出,祥云朵朵。山中小调,曲静幽雅。 银线高塔,巍峨耸立。雨刷过后,一片忙碌。
    曹国泽阅读 578评论 2 1
  • 最近这个事件被炒的沸沸扬扬,大部分人都偏向宝宝,都在谴责马蓉。大部分人都只是道听途说,一边倒,我想说婚姻出现问题了...
    圭陌阅读 830评论 8 5
  • 安阳和小夏曾是一对令人羡慕的情侣,从青葱岁月到而立之年,眼看要修成正果了,却遭到小夏妈妈棒打鸳鸯,最后,不得已而忍...
    点墨未央阅读 189评论 0 1