TCP Socket 编程

套接字Socket

Socket 通常也称作套接字,网络上的两个程序通过一个双向链路实现数据的交换,这个双向链路的一端称为一个 Socket,由IP地址和端口号唯一确定。应用程序通过套接字向网络发出请求或者应答网络请求。

Socket 是对 TCP/IP 协议的封装,其本身并不是协议,而是一个调用接口(API)。大多数的API提供者(如操作系统,JDK)往往会提供基于这种概念的接口。

TCP/IP 中主要 socket 类型为流套接字(使用 TCP 协议)和数据报套接字(使用 UDP 协议)。

Java 为 TCP 协议提供了两个类:Socket 类和 ServerSocket 类。Socket 实例代表客户端,ServerSocket 实例代表服务器端,客户端与服务器端的关系是一对多的关系。


客户端与服务端的连接步骤

客户端执行如下三步操作(参考Java TCP Socket 编程):

  • 创建 Socket 实例:构造函数向指定的远程主机和端口建立 TCP 连接;
  • 通过 Socket 的 I/O 流与服务端通信;
  • 通信完成后,使用 Socket 类的 close() 方法关闭连接。

服务端执行如下两步操作:

  • 创建一个 ServerSocket 实例并指定本地端口,用来监听客户端在该端口发送的 TCP 连接请求;
  • 调用 ServerSocket 的 accept()方法获取客户端连接,并通过其返回值创建一个 Socket 实例;
  • 通过 Socket 实例的 I/O 流与客户端通信;
  • 通信完成后,使用 Socket 类的 close() 方法关闭连接。

一对一Socket Demo

简单的 socket 通信实验可以在自己的电脑上就可以完成。如下是一个一对一的 socket 通信例子。

客户端:

public class ClientSocket {
    private static String ip = "127.0.0.1";
    private static int port = 2017;

    public static void main(String[] args) throws IOException {
        ClientSocket client = new ClientSocket();
        client.init();
    }

    public void init() throws IOException {
        System.out.println("客户端初始化完成!");
        Socket client = new Socket(ip, port);
        client.setSoTimeout(5000);//超时时间为5s
        System.out.println("服务器端初始化完成!");
        BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
        PrintWriter out = new PrintWriter(client.getOutputStream());
        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.print("请输入要发送的数据:");
            // out.write("你好\n");
            out.println(sc.nextLine());
            out.flush();
            String msg = in.readLine();
            if (msg != null) {
                if (msg.equals("end")) {
                    out.println("end");
                    out.flush();
                    System.out.println("本次连接结束!");
                    break;
                }
                System.out.println(msg);
            }
        }
        sc.close();
        out.close();
        in.close();
        client.close();
    }
}

服务器端:

public class SocketServer {
    private static final int SERVER_PORT = 2017;

    public static void main(String[] args) throws IOException {
        SocketServer server = new SocketServer();
        server.serverInit();
    }

    public void serverInit() throws IOException {
        @SuppressWarnings("resource")
        ServerSocket socket = new ServerSocket(SERVER_PORT);
        Socket server = socket.accept();
        BufferedReader in = new BufferedReader(new InputStreamReader(server.getInputStream()));
        PrintWriter out = new PrintWriter(server.getOutputStream());
        while (true) {
            String msg = in.readLine();
            if (msg != null) {
                if (msg.equals("end")) {
                    out.println("end");
                    out.flush();
                    break;
                }
                out.println("服务器:好的!");
                out.flush();
            }

        }
        out.close();
        in.close();
        server.close();
    }
}

输出结果:

客户端初始化完成!
服务器端初始化完成!
请输入要发送的数据:hello
服务器:好的!
请输入要发送的数据:end
本次连接结束!

注意点

  • 必须先启动服务器端,再启动客户端
  • 向另一端写数据时如果用的是 PrintWriter 或其它带缓冲的IO流时必须调用 flush() 方法以立即刷新数据
  • 服务器端和客户端的套接字要对应
  • 如果用的是 write() 方法写入字符串,另一端采用 readline() 方法读取时,需要在字符串最后加上换行符(参考代码中被注释的语句),println() 方法已经自动帮我们添加了换行符
  • 如果提示端口已被占用,查看 console 中是否有未结束的进程(一般有服务器端和客户端两个 console),或者查看任务管理器结束多余的 Java(TM) Platform SE binary 进程,如果还不行就换个端口

非并发多对一Socket Demo

虽然上面的程序实现了 Socket 通信的功能,可是只能满足一个客户端与服务器端通信的要求。当一个客户端发送 end 后,服务器端也被终止了。

为了满足同时多台客户端连接服务器端的要求,我们需要对服务器端做一些改进。

public class SocketServer2 {
    private static final int SERVER_PORT = 2017;

    public static void main(String[] args) throws IOException {
        SocketServer server = new SocketServer();
        server.serverInit();
    }

    public void serverInit() throws IOException {
        @SuppressWarnings("resource")
        ServerSocket socket = new ServerSocket(SERVER_PORT);
        while (true) {
            Socket server = socket.accept();
            BufferedReader in = new BufferedReader(new InputStreamReader(server.getInputStream()));
            PrintWriter out = new PrintWriter(server.getOutputStream());
            while (true) {
                String msg = in.readLine();
                if (msg != null) {
                    if (msg.equals("end")) {
                        out.println("end");
                        out.flush();
                        break;
                    }
                    out.println("服务器:好的!");
                    out.flush();
                }

            }
            out.close();
            in.close();
            server.close();
        }
    }
}

客户端代码不变。可以看到,只是又加了一层 while 循环。这样当一个客户端发送 end 后,其它客户端仍能连接到服务器端。


并发多对一Socket Demo

但现在又有一个问题,只有当前正在通信的客户端发送 end 后,其它客户端才能与服务器端通信,也就是其它客户端会被阻塞。原因是 ServerSocket的accept() 方法是阻塞的。

如何让多个客户端同时与服务器端通信呢?这就要靠多线程了。

public class SocketServer3 {
    private static final int SERVER_PORT = 2017;

    public static void main(String[] args) throws IOException {
        SocketServer server = new SocketServer();
        server.serverInit();
    }

    public void serverInit() throws IOException {
        @SuppressWarnings("resource")
        ServerSocket socket = new ServerSocket(SERVER_PORT);
        while (true) {
            Socket server = socket.accept();
            new Thread(new Dispatch(server)).start();
        }
    }

    static class Dispatch implements Runnable {
        Socket innerServer;

        Dispatch(Socket socket) {
            this.innerServer = socket;
        }

        @Override
        public void run() {
            try (BufferedReader in = new BufferedReader(new InputStreamReader(innerServer.getInputStream()));
                 PrintWriter out = new PrintWriter(innerServer.getOutputStream())) {
                while (true) {
                    String msg = in.readLine();
                    if (msg != null) {
                        if (msg.equals("end")) {
                            out.println("end");
                            out.flush();
                            break;
                        }
                        out.println("服务器:好的!");
                        out.flush();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

将业务逻辑封装到一个内部类中,内部类实现了 Runnable 接口,当然也可以选择继承 Thread。资源的关闭选择使用了 try-with-resources 处理。这样就实现了多个客户端可以同时与服务器端通信的目的。

事实上,当客户端逐渐增多的时候,建立的线程也逐渐增多,而服务器的线程是有限的,过多的线程造成的线程间切换的消耗也会相当的大。因此这种方式在并发量大的场景下无法承载。可以使用线程池来解决这个问题。


线程池Socket Demo

在 java.util.concurrent 包下,提供了一系列与线程池相关的类。一般通过工具类 Executors 的静态方法来获取线程池。

Executors 提供了四种线程池,这里选择了 FixedThreadPool。它会创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。此时如果有新任务到来,会将任务加入到无界队列 LinkedBlockingQueue 等待。

线程池的大小要根据任务特性决定,一般如果是CPU密集型,线程数量为N+1(N为CPU核数),如果是IO密集型,线程数量为2*N。这里我用的线程池大小是8。

示例如下:

public class SocketServer {
    private static final int SERVER_PORT = 2017;

    public static void main(String[] args) throws IOException {
        SocketServer server = new SocketServer();
        server.serverInit();
    }

    public void serverInit() throws IOException {
        @SuppressWarnings("resource")
        ServerSocket socket = new ServerSocket(SERVER_PORT);
        ExecutorService exec = Executors.newFixedThreadPool(8);
        while (true) {
            Socket server = socket.accept();
            exec.execute(new Thread(new Dispatch(server)));
        }
    }

    static class Dispatch implements Runnable {
        Socket innerServer;

        Dispatch(Socket socket) {
            this.innerServer = socket;
        }

        @Override
        public void run() {
            try (BufferedReader in = new BufferedReader(new InputStreamReader(innerServer.getInputStream()));
                 PrintWriter out = new PrintWriter(innerServer.getOutputStream())) {
                while (true) {
                    String msg = in.readLine();
                    if (msg != null) {
                        if (msg.equals("end")) {
                            out.println("end");
                            out.flush();
                            break;
                        }
                        out.println("服务器:好的!");
                        out.flush();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

考虑一个聊天服务器,可能有上千个客户端同时连接到服务器,但是在任何时刻只有非常少量的消息需要读取和分发,如果采用线程池会非常浪费资源,这时候采用NIO会有更好的效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值