第一次学习 Socket 编程

服务器端:

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;

public class Server {
    // 绑定地址
    private static String ADDRESS = "127.0.0.1";

    // 监听端口号
    private static final int PORT = 8080;

    public static void main(String[] args) throws IOException {
        // 创建 server 对象
        Server server = new Server();
        // 调用 service() 方法,等待请求
        server.service();
    }

    public void service() {
        // 初始化
        ServerSocket server = null;
        try {
            // 创建 InetAddress 对象简单的方法是调用其静态方法 getByName()
            InetAddress adr = InetAddress.getByName(ADDRESS);

            // 创建 ServerSocket 对象
            // 参数说明:1.监听端口号 2.请求最大队列长度 3.绑定地址
            server = new ServerSocket(PORT, 5, adr);

            System.out.println("服务启动成功");
        } catch (IOException e) {
            System.out.println("服务启动失败");
            e.printStackTrace();
        }

        // 循环等待请求
        while (true) {
            // 初始化参数
            Socket socket = null;

            try {
                // accept 方法会从连接请求队列中取出一个连接请求,然后创建与客户端连接的 Socket 对象,并将它返回
                // 如果队列中没有连接请求,accept() 方法就会一直等待,直到接收到了连接请求才返回
                socket = server.accept();

                // 打印请求地址和端口
                System.out.println("连接来自:" + socket.getLocalSocketAddress());

                // 解析客户端发来的请求
                String request = parse(socket.getInputStream());

                // 对请求进行处理
                String response = process(request);

                // 将请求结果发送给客户端
                send(socket.getOutputStream(), response);

                // 关闭当前的 socket
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
                // 出错时重新开始新的循环
                continue;
            }
        }
    }

    // 返回值可以认为是一个 Request 的对象
    protected String parse(InputStream input) {

        int len;
        String charset = "UTF-8";
        byte[] buffer = new byte[2048];

        try {
            // 一次读取全部的请求信息(因为请求一般不会太长)
            len = input.read(buffer);

            // 解析处理(暂无)
            // TODO

            // 解析结果返回
            return new String(buffer, 0, len, charset);
        } catch (IOException e) {
            e.printStackTrace();
            return "解析请求时发生异常!";
        }
    }

    // 对请求进行处理,并返回处理结果
    protected String process(String request) {
        // 这里只做了简单的处理
        return "来自服务端的消息:\r\n" + request;
    }

    // 这里的第二个参数,可以理解为 Response 对象
    protected void send(OutputStream output, String response) {
        // 创建 PrintWriter 对象,第二个参数为 true,则 println、printf 或 format 方法将刷新输出缓冲区
        PrintWriter out = new PrintWriter(output, true);

        // 将 response 发送给客户端
        out.println(response);
    }
}

客户端:

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class Client {

    public static void main(String[] args) {
        // 连接到的主机地址
        String host = "127.0.0.1";
        // 连接的端口号
        int port = 8080;

        try {
            // 创建连接服务器的 Socket 对象
            Socket socket = new Socket(host, port);

            // 要从连接的另一端接受字节流,需要调用 Socket 类的 getInputStream 方法获取 InputStream 对象,
            // 之后可以将其作为参数创建需要的 BufferedReader 对象
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            // 需要发送字节流时,需要调用 Socket 类的 getOutputStream 方法获取 OutputStream 对象,
            // 之后可以将其用来创建需要的 PrintWriter 对象
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

            // 向服务器端发送请求
            out.println("Hello World!");

            // 接受服务器端发送回来的信息
            while (true) {
                // ready() 方法判断此数据流是否已准备好被读取,这里主要是判断缓冲区是否为空
                if (in.ready()) {
                    // 缓冲区不为空时,按行读取流中数据
                    String line;
                    while ((line = in.readLine()) != null) {
                        System.out.println(line);
                    }
                    // 读取结束后跳出循环
                    break;
                }
                // 缓冲区为空,则线程等待 50 毫秒(等待服务器发送数据)
                Thread.sleep(50);
            }

            // 关闭 socket
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

知识点补充:

1. 构造 ServerSocket

        ServerSocket 构造方法有以下四种形式:

  • ServerSocket()throws IOException
  • ServerSocket(int port) throws IOException
  • ServerSocket(int port, int backlog) throws IOException
  • ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

        在以上构造方法中,参数 port 指定服务器要绑定的端口(监听端口),参数 backlog 指定客户端连接请求队列的长度,参数 bindAddr 指定服务端要绑定的 IP 地址。


1.1 绑定端口

       除了第一个不带参数的构造方法之外,其它构造方法都会使服务器和特定端口绑定,端口由参数 port 指定,当运行时无法绑定到某端口时,会抛出 IOException, 更确切的说是 BindException,它是 IOException 的子类。BindException 一般有以下几个原因造成:

        a)端口已经被其它服务器进程占用

        b)在某些操作系统中,如果没有以超级用户的身份来运行服务器程序,那么操作系统不允许服务器绑定到 1~1023 端口

        如果把参数 port 设为 0,表示由操作系统分配任意一个可用的端口。由服务器分配的端口也称之为匿名端口。对于多数服务器,会使用明确的端口,因为客户程序需要事先知道服务器端口,才能方便的访问服务器。在某些场合才会使用到匿名端口,比如 FTP(文件传输)协议等。


1.2 设定客户连接请求队列的长度

        当服务器进程运行时,可能会同时监听到多个客户的连接请求。管理这些请求的任务是由操作系统来完成的,操作系统会把这些连接请求存储在一个先进先出的队列中。很多系统限定了队列的最大长度为 50。当队列中的请求达到了队列的最大容量时,服务器进程所在的主机就会拒绝新的连接请求。只有当服务器进程通过 ServerSocket 的accep() 方法从队列中取出连接请求,使队列腾出空位时,队列才能继续加入新的连接请求。

        对于客户端进程,如果它发出的连接请求被加入到服务器的队列中,就意味着客户和服务器的连接建立成功,客户进程从 Socket 构造方法中正常返回。

        ServerSocket 构造方法的 backlog 参数用来显示设置连接请求队列的长度,它将覆盖系统设定的最大长度,但在以下情况,仍然会采用系统限定的队列的最大长度:

        a)backlog 参数的值大于系统限定的队列的最大长度

        b)backlog 参数的值小于或等于 0

        c)在 ServerSocket 构造函数中没有设置 backlog 参数


1.3 设定绑定的 IP 地址

        如果主机只有一个 IP 地址,那么默认情况下,服务器就会与该 IP 地址绑定。ServerSocket 4个构造方法中只有第四个显示指定服务器要绑定的 IP 地址,该构造方法适用于具有多个 IP 地址的主机。


1.4 默认构造方法的作用

        ServerSocket 有一个不带任何参数的默认构造方法,通过该构造方法创建的 ServerSocket 不和任何端口绑定,但是之后需要通过 bind() 方法与特定端口绑定。这么做的目的是,允许服务器在绑定到特定端口之前,先设置 ServerSocket 的一些选项。因为一点服务器与特定端口绑定,有些选项就不能在改变了。

        例如:

ServerSocket serverSocket=new ServerSocket();
serverSocket.setReuseAddress(true); // 设置 ServerSocket 选项
serverSocket.bind(new InetSocketAddress(8080)); // 与8080端口绑定
        如果把 2、3 句代码互换,那么设置 ServerSocket 选项就完全不起作用了。

        ServerSocket 有以下 3 个选项:

  • SO_TIMEOUT:表示等待客户连接的超时时间
  • SO_REUSEADDR:表示是否允许重用服务器所绑定的地址
  • SO_RCVBUF:表示接收数据的缓冲区的大小
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值