Socket笔记(第二弹:Java Socket通信的基本实现)

套接字的低级细节相当棘手。幸运的是,Java 平台给了我们一些虽然简单但却强大的更高级抽象,使我们可以容易地创建和使用套接字。

Java有两个基于数据流的套接字类:
ServerSocket:服务器用它监听进入的连接。监听套接字只能接收新的连接请求,不能接收实际的数据包,即ServerSocket不能接收实际的数据包。
Socket:Socket是基类,它支持TCP协议。TCP是一个可靠的流网络连接协议。Socket类提供了流输入/输出的方法,使得从套接字中读出数据和往套接字中写数据都很容易。
我们用ServerSocket、Socket类创建一个套接字连接,从套接字得到的结果是一个InputStream以及OutputStream对象,以便将连接作为一个IO流对象对待。通过IO流可以从流中读取数据或者写数据到流中。

基本场景:客户端写数据,服务端读数据

万事开头难,为了让开头也不那么难,我们先来一段最简单的Java Socket通信代码,该代码实现了客户端写数据,服务端读数据的基本场景。
服务端代码:

package com.hello.tcp;

import java.io.InputStreamReader;
import java.io.Reader;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws Exception {
        int port = 39000;
        // 定义一个ServerSocket,监听在端口port上 
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("Server start:" + port);

        // 阻塞,直到有客户端连接
        Socket socket = serverSocket.accept();
        System.out.println("Accept the client: " + socket);

        // 读取客户端发来的数据
        Reader reader = new InputStreamReader(socket.getInputStream());
        StringBuilder sb = new StringBuilder("");
        char[] chars = new char[64];
        int len;
        while((len = reader.read(chars)) != -1) {
            sb.append(new String(chars, 0, len));
        }
        System.out.println(sb.toString());

        reader.close();
        socket.close();
        serverSocket.close();
    }

}

客户端代码:

package com.hello.tcp;

import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;

public class Client {

    public static void main(String[] args) throws Exception {
        String ip = "127.0.0.1";
        int port = 39000;
        // 与服务端建立连接
        Socket socket = new Socket(ip, port);

        // 往服务端写数据
        Writer writer = new OutputStreamWriter(socket.getOutputStream());
        writer.write("Hello server! I come from the client!");
        writer.flush();

        writer.close();
        socket.close();
    }

}

以上代码做了这样的事情:
服务端:
1、初始化一个ServerSocket监听某个端口,并调用accept方法进行阻塞,等待客户端连接;
2、当发现客户端有Socket来试图连接它时,它会accept该Socket的连接请求,同时在服务端建立一个对应的Socket与之进行通信;
3、读取客户端发来的数据。服务端从Socket的InputStream中读取数据的操作也是阻塞式的;
4、关闭对应的资源,即关闭对应的IO流和Socket。
客户端:
1、与服务端建立连接;
2、往服务端写数据;
3、关闭对应的资源。

我们先运行服务端代码,然后运行客户端代码,可以看到服务端控制台显示如下信息:
Server start:39000
Accept the client: Socket[addr=/127.0.0.1,port=52065,localport=39000]
Here comes the message from the client: Hello server! I come from the client!

客户端写入的数据服务端已成功接收到,客户端写,服务端读的场景成功实现。

改进:客户端发送数据,服务端接收,然后服务端发回数据,客户端接收

Socket之间是可以双向通信的,上面我们实现的是客户端写,服务端读的场景,下面我们稍作修改,让客户端和服务器都进行写和读的操作。
那么肯定有人说了,那还不简单啊,服务端在读数据的代码之后加一段写数据的代码,客户端在写数据的代码之后加一段读数据的代码不就好了。好,我们来试一下:
服务端代码:

package com.hello.tcp;

import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws Exception {
        int port = 39000;
        // 定义一个ServerSocket,监听在端口port上 
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("Server start:" + port);

        // 阻塞,直到有客户端连接
        Socket socket = serverSocket.accept();
        System.out.println("Accept the client: " + socket);

        // 读取客户端发来的数据
        Reader reader = new InputStreamReader(socket.getInputStream());
        StringBuilder sb = new StringBuilder("");
        char[] chars = new char[64];
        int len;
        while((len = reader.read(chars)) != -1) {
            sb.append(new String(chars, 0, len));
        }
        System.out.println("Here comes the message from the client: " + sb.toString());

        // 给客户端发数据
        Writer writer = new OutputStreamWriter(socket.getOutputStream());
        writer.write("Hello client! I come from the server!");
        writer.flush();

        writer.close();
        reader.close();
        socket.close();
        serverSocket.close();
    }

}

客户端代码:

package com.hello.tcp;

import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;

public class Client {

    public static void main(String[] args) throws Exception {
        String ip = "127.0.0.1";
        int port = 39000;
        // 与服务端建立连接
        Socket socket = new Socket(ip, port);

        // 往服务端写数据
        Writer writer = new OutputStreamWriter(socket.getOutputStream());
        writer.write("Hello server! I come from the client!");
        writer.flush();

        // 读取服务端发来的数据
        Reader reader = new InputStreamReader(socket.getInputStream());
        StringBuilder sb = new StringBuilder("");
        char[] chars = new char[64];
        int len;
        while((len = reader.read(chars)) != -1) {
            sb.append(new String(chars, 0, len));
        }
        System.out.println("Here comes the message from the server: " + sb.toString());

        reader.close();
        writer.close();
        socket.close();
    }

}

我们运行服务端,然后运行客户端,发现服务端控制台显示如下信息:
Server start:39000
Accept the client: Socket[addr=/127.0.0.1,port=52506,localport=39000]
而客户端控制台什么都没有显示。这和预想的不一样。
在上述代码中,服务端从输入流中读取客户端发送过来的数据,然后再往输出流里面写入数据给客户端,最后关闭对应的资源文件。而实际上上述代码可能并不会按照我们预想的方式运行,因为从输入流中读取数据是一个阻塞式操作,在服务端读数据的while循环中当读到数据的时候就会执行循环体,否则就会阻塞,后面的代码不会执行。除非客户端对应的Socket关闭了阻塞才会停止,while循环也才会跳出。而这个时候客户端也在读数据的地方阻塞,这样我们就进入了两边无限互相等待的情况。
针对这种可能永远无法执行下去的情况的解决方法是while循环需要在里面有条件的跳出来,通常我们都会约定一个结束标记,当发送过来的数据包含某个结束标记时就说明当前的数据已经发送完毕了,这个时候我们就可以进行循环的跳出。
我们稍作修改:
服务端代码:

package com.hello.tcp;

import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws Exception {
        int port = 39000;
        // 定义一个ServerSocket,监听在端口port上 
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("Server start:" + port);

        // 阻塞,直到有客户端连接
        Socket socket = serverSocket.accept();
        System.out.println("Accept the client: " + socket);

        // 读取客户端发来的数据
        Reader reader = new InputStreamReader(socket.getInputStream());
        StringBuilder sb = new StringBuilder("");
        char[] chars = new char[64];
        int len;
        String temp;
        while((len = reader.read(chars)) != -1) {
            temp = new String(chars, 0, len);
            if (temp.indexOf("eof") != -1) {
                sb.append(temp.substring(0, temp.indexOf("eof")));
                break;
            }
            sb.append(temp);
        }
        System.out.println("Here comes the message from the client: " + sb.toString());

        // 给客户端发数据
        Writer writer = new OutputStreamWriter(socket.getOutputStream());
        writer.write("Hello client! I come from the server!");
        writer.write("eof");
        writer.flush();

        writer.close();
        reader.close();
        socket.close();
        serverSocket.close();
    }

}

客户端代码:

package com.hello.tcp;

import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;

public class Client {

    public static void main(String[] args) throws Exception {
        String ip = "127.0.0.1";
        int port = 39000;
        // 与服务端建立连接
        Socket socket = new Socket(ip, port);

        // 往服务端写数据
        Writer writer = new OutputStreamWriter(socket.getOutputStream());
        writer.write("Hello server! I come from the client!");
        writer.write("eof");
        writer.flush();

        // 读取服务端发来的数据
        Reader reader = new InputStreamReader(socket.getInputStream());
        StringBuilder sb = new StringBuilder("");
        char[] chars = new char[64];
        int len;
        String temp;
        while((len = reader.read(chars)) != -1) {
            temp = new String(chars, 0, len);
            if (temp.indexOf("eof") != -1) {
                sb.append(temp.substring(0, temp.indexOf("eof")));
                break;
            }
            sb.append(temp);
        }
        System.out.println("Here comes the message from the server: " + sb.toString());

        reader.close();
        writer.close();
        socket.close();
    }

}

运行以后,服务端控制台显示:
Server start:39000
Accept the client: Socket[addr=/127.0.0.1,port=53964,localport=39000]
Here comes the message from the client: Hello server! I come from the client!
客户端控制台显示:
Here comes the message from the server: Hello client! I come from the server!
好了,客户端和服务端都读和写的场景成功实现。

改进:读数据时一次读一行

以上代码添加结束标记的方法也有缺陷,我们所使用的结束标记“eof”如果正好被拆开读取了,比如一次循环读取到“xxxxxeo”,下一次循环读取到“f”,那么这个结束标记就没用了。而且在从Socket的InputStream中接收数据时,用Reader一点点的读也比较麻烦。于是我们改进一下,使用BufferedReader来一次读一行,这样既不会让结束标记被拆开读取,又能读得更方便一点。
服务端代码:

package com.hello.tcp;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws Exception {
        int port = 39000;
        // 定义一个ServerSocket,监听在端口port上 
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("Server start:" + port);

        // 阻塞,直到有客户端连接
        Socket socket = serverSocket.accept();
        System.out.println("Accept the client: " + socket);

        // 读取客户端发来的数据
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        StringBuilder sb = new StringBuilder("");
        String temp;
        while((temp = bufferedReader.readLine()) != null) {
            if (temp.indexOf("eof") != -1) {
                sb.append(temp.substring(0, temp.indexOf("eof")));
                break;
            }
            sb.append(temp);
        }
        System.out.println("Here comes the message from the client: " + sb.toString());

        // 给客户端发数据
        Writer writer = new OutputStreamWriter(socket.getOutputStream());
        writer.write("Hello client! I come from the server!\n");
        writer.write("eof\n");
        writer.flush();

        writer.close();
        bufferedReader.close();
        socket.close();
        serverSocket.close();
    }

}

客户端代码:

package com.hello.tcp;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;

public class Client {

    public static void main(String[] args) throws Exception {
        String ip = "127.0.0.1";
        int port = 39000;
        // 与服务端建立连接
        Socket socket = new Socket(ip, port);

        // 往服务端写数据
        Writer writer = new OutputStreamWriter(socket.getOutputStream());
        writer.write("Hello server! I come from the client!\n");
        writer.write("eof\n");
        writer.flush();

        // 读取服务端发来的数据
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        StringBuilder sb = new StringBuilder("");
        String temp;
        while((temp = bufferedReader.readLine()) != null) {
            if (temp.indexOf("eof") != -1) {
                sb.append(temp.substring(0, temp.indexOf("eof")));
                break;
            }
            sb.append(temp);
        }
        System.out.println("Here comes the message from the server: " + sb.toString());

        bufferedReader.close();
        writer.close();
        socket.close();
    }

}

BufferedReader的readLine方法是一次读一行的,这个方法是阻塞的,直到它读到了一行数据为止程序才会继续往下执行。程序遇到了换行符或者是对应流的结束符readLine方法才会认为读到了一行,才会结束其阻塞,让程序继续往下执行。所以我们输出流里面写入了换行符“\n”。

改进:一个服务端接收多个客户端的请求

以上代码实现的客户端发送数据,服务端接收,然后服务端发回数据,客户端接收的场景比较常用,但是在服务端接收了一个客户端的请求以后就关闭了所有资源结束了,而实际上一个服务端需要接收多个客户端的请求才能满足我们的需求,所以我们再次进行改进,让一个服务端能接收多个客户端的请求。
服务端代码:

package com.hello.tcp;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws Exception {
        int port = 39000;
        // 定义一个ServerSocket,监听在端口port上 
        @SuppressWarnings("resource")
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("Server start:" + port);

        while (true) {
            // 阻塞,直到有客户端连接
            Socket socket = serverSocket.accept();
            System.out.println("Accept the client: " + socket);

            // 读取客户端发来的数据
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            StringBuilder sb = new StringBuilder("");
            String temp;
            while((temp = bufferedReader.readLine()) != null) {
                if (temp.indexOf("eof") != -1) {
                    sb.append(temp.substring(0, temp.indexOf("eof")));
                    break;
                }
                sb.append(temp);
            }
            System.out.println("Here comes the message from the client: " + sb.toString());

            // 给客户端发数据
            Writer writer = new OutputStreamWriter(socket.getOutputStream());
            writer.write("Hello client! I come from the server!\n");
            writer.write("eof\n");
            writer.flush();

            writer.close();
            bufferedReader.close();
            socket.close();
        }
    }

}

上述代码中我们用了一个死循环,在循环体里面accept阻塞,等待客户端的连接请求。接收到请求的时候,跟客户端进行通信,完了后会继续accept阻塞。等待下一个请求。这样就实现了一个服务端接收多个客户端的请求的场景。

改进:异步处理与客户端的通信

上面代码中,我们的服务端处理客户端的连接请求是同步进行的,每次接收到来自客户端的连接请求后,都要先跟当前的客户端通信完之后才能再处理下一个连接请求。这在并发比较多的情况下会严重影响程序的性能,为此,我们可以把它改为异步处理与客户端通信。
服务端代码:

package com.hello.tcp;

import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) throws Exception {
        int port = 39000;
        // 定义一个ServerSocket,监听在端口port上 
        @SuppressWarnings("resource")
        ServerSocket serverSocket = new ServerSocket(port);
        System.out.println("Server start:" + port);

        while (true) {
            // 阻塞,直到有客户端连接
            Socket socket = serverSocket.accept();
            System.out.println("Accept the client: " + socket);
            // 建立一个新的线程来处理接收到的请求 
            new Thread(new SocketHandler(socket)).start();
        }
    }

}
package com.hello.tcp;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;

public class SocketHandler implements Runnable {

    private Socket socket;

    public SocketHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 读取客户端发来的数据
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            StringBuilder sb = new StringBuilder("");
            String temp;
            while((temp = bufferedReader.readLine()) != null) {
                if (temp.indexOf("eof") != -1) {
                    sb.append(temp.substring(0, temp.indexOf("eof")));
                    break;
                }
                sb.append(temp);
            }
            System.out.println("Here comes the message from the client: " + sb.toString());

            // 给客户端发数据
            Writer writer = new OutputStreamWriter(socket.getOutputStream());
            writer.write("Hello client! I come from the server!\n");
            writer.write("eof\n");
            writer.flush();

            writer.close();
            bufferedReader.close();
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

每次ServerSocket接收到一个新的Socket连接请求后都会新起一个线程来跟当前Socket进行通信,这里新建了一个SocketHandler类,实现了Runnable接口。

改进:设置超时时间

有时,出于某些考虑,我们需要服务端在一定时间内响应,那么我们就要在请求达到一定的时间后控制阻塞的中断,让程序得以继续运行。我们可以使用Socket的setSoTimeout()方法来设置接收数据的超时时间,单位是毫秒。
客户端代码:

package com.hello.tcp;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;
import java.net.SocketTimeoutException;

public class Client {

    public static void main(String[] args) throws Exception {
        String ip = "127.0.0.1";
        int port = 39000;
        // 与服务端建立连接
        Socket socket = new Socket(ip, port);
        // 设置超时时间
        socket.setSoTimeout(5*1000);

        // 往服务端写数据
        Writer writer = new OutputStreamWriter(socket.getOutputStream());
        writer.write("Hello server! I come from the client!\n");
        writer.write("eof\n");
        writer.flush();

        // 读取服务端发来的数据
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        StringBuilder sb = new StringBuilder("");
        String temp;
        try {
            while((temp = bufferedReader.readLine()) != null) {
                if (temp.indexOf("eof") != -1) {
                    sb.append(temp.substring(0, temp.indexOf("eof")));
                    break;
                }
                sb.append(temp);
            }
        } catch (SocketTimeoutException e) {
            System.out.println("Time out!");
        }

        System.out.println("Here comes the message from the server: " + sb.toString());

        bufferedReader.close();
        writer.close();
        socket.close();
    }

}

这样,在读取数据的时候,如果超过了设置的超时时间,Socket就会抛出一个SocketTimeoutException,停止阻塞。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值