NIO与网络编程——实现Socket通信

PunkLu 2020年01月07日 51次浏览
实现Socket通信
# 实现Socket通信

基于TCP的Socket通信

TCP提供基于“流”的“长连接”的数据传递,发送的数据带有顺序性。TCP是一种流协议,以流为单位进行数据传输。

在TCP/IP中,连接可以认为是服务端与客户端确认彼此都存在的过程。这个过程需要实现,就要创建连接,创建连接需要服务端与客户端进行三次握手,握手成功后,说明服务端与客户端之前能实现数据通信。如果建立连接的过程是成功的,就说明连接被成功创建。在创建好的一个连接中,使用TCP可以实现多次的数据通信。在多次数据通信的过程中,服务端与客户端要进行彼此都存在的过程验证,也就是验证连接是否正常,如果连接正常,并且多次通信,则这就是长连接。长连接就是复用当前的连接以达到多次通信的目的。由于复用当前的连接进行数据通信,因此不需要重复创建连接,传输效率比较高。而当实现一次数据通信之后,关闭连接,这种情况就是短连接。使用短连接进行数据传输时,由于每次传输数据前都要创建连接,这样会产生多个连接对象,增大占用内存的空间,在创建连接时也要进行服务端和客户端之前彼此确认存在,确认的过程比较耗时,因此效率较低。由于UDP是无连接协议,也就是服务端与客户端没有确认彼此都存在的握手过程,因此在UDP里不存在长连接短连接的概念。

验证ServerSocket类的accept()方法具有阻塞特性

ServerSocket类的作用是创建Socket(套接字)的服务端,而Socket类的作用是创建Socket的客户端。在代码层面使用的方式就是使用Socket类去连接ServerSocket类,也就是客户端要主动连接服务端。

ServerSocket类中的public Socket accept()方法的作用是侦听并接受此套接字的连接。此方法在连接传入(客户端连接)之前一直阻塞。public Socket accept()方法的返回值是Socket类型。

验证阻塞特性的服务端代码:

public class Server {

    public static void main(String[] args) {
        try {
            // 设置服务器的Socket端口号
            ServerSocket socket = new ServerSocket(8088);
            System.out.println("server阻塞开始=" + System.currentTimeMillis());
            socket.accept();
            System.out.println("server阻塞结束=" + System.currentTimeMillis());
            socket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

验证阻塞特性的客户端代码:

public class Client {

    public static void main(String[] args) {
        try {
            System.out.println("client连接准备=" + System.currentTimeMillis());
            // 设置此客户端要连接的ip地址及端口
            Socket socket = new Socket("localhost",8088);
            System.out.println("client连接结束=" + System.currentTimeMillis());
            socket.close();
        }catch (UnknownHostException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

以下示例代码演示了ServerSocket和Socket类的使用:

public class TestWebByServerSocket {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket= new ServerSocket(8080);
        Socket socket = serverSocket.accept();
        InputStream inputStream = socket.getInputStream();
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

        String getString = "";
        while (!"".equals(getString = bufferedReader.readLine())){
            System.out.println(getString);
        }

        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("HTTP/1.1 200 OK\r\n\r\n".getBytes());
        outputStream.write(
                "<html><body><a href='http://www.baidu.com'>i am baidu.com welcome you!</a></body></html>".getBytes()
        );

        outputStream.flush();
        inputStream.close();
        outputStream.close();
        socket.close();
        serverSocket.close();
    }
}

以上代码启动后,在浏览器输入127.0.0.1:8080并回车访问。页面上显示i am baidu.com welcome you!的跳转向百度的超链接。控制台输出以下信息:

GET / HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36
Sec-Fetch-User: ?1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,zh;q=0.8,zh-CN;q=0.7

验证Socket中InputStream类的read()方法也具有阻塞特性

除了ServerSocket类中的accept()方法具有阻塞特性外,InputStream类中的read()方法也同样具有阻塞特性。

验证ServerSocket类中的InputStream的read()方法具有阻塞特性的服务端代码:

public class TestSocketInputStreamBlockServer {


    public static void main(String[] args) {
        try {
            byte[] byteArray = new byte[1024];
            ServerSocket serverSocket = new ServerSocket(8088);
            System.out.println("accept begin " + System.currentTimeMillis());
            Socket socket= serverSocket.accept(); // 呈阻塞效果
            System.out.println("accept end " + System.currentTimeMillis());

            InputStream inputStream = socket.getInputStream();
            System.out.println("read begin " + System.currentTimeMillis());
            int readLength = inputStream.read(byteArray); // 呈阻塞效果
            System.out.println("read end " + System.currentTimeMillis());
            inputStream.close();
            socket.close();
            serverSocket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

验证ServerSocket类中的InputStream的read()方法具有阻塞特性的客户端代码:

public class TestSocketInputStreamBlockClient {

    public static void main(String[] args) {
        try {
            System.out.println("socket begin " + System.currentTimeMillis());
            Socket socket = new Socket("localhost",8088);
            System.out.println("socket end " + System.currentTimeMillis());
            Thread.sleep(Integer.MAX_VALUE);
            socket.close();
        }catch (IOException e){
            e.printStackTrace();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

首先执行TestSocketInputStreamBlockServer类,可以发现ServerSocket的accept()方法具有阻塞特性,然后执行TestSocketInputStreamBlockClient方法,会发现客户端运行结束但进程并未销毁。再次查看TestSocketInputStreamBlockServer的控制台,会发现服务端在Socket获得的InputStream的read方法处阻塞,read()方法阻塞的原因是客户端并未发送数据到服务端,服务端一直在尝试读取从客户端传递过来的数据,因为客户端从未发送数据给客户端,所以服务端一直在阻塞。

客户端向服务端传递字符串

真正实现服务端与客户端进行通信。

服务端代码:

public class TestClientDeliverStringToServerServer {

    public static void main(String[] args) {
        try {
            char[] charArray = new char[3];
            ServerSocket serverSocket = new ServerSocket(8088);
            System.out.println("accept begin " + System.currentTimeMillis());
            Socket socket = serverSocket.accept();
            System.out.println("accept end " + System.currentTimeMillis());

            InputStream inputStream = socket.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            System.out.println("read begin " + System.currentTimeMillis());
            int readLength = inputStreamReader.read(charArray);

            while (readLength != -1){
                String newString = new String(charArray,0,readLength);
                System.out.println(newString);
                readLength = inputStreamReader.read(charArray);
            }
            System.out.println("read end " + System.currentTimeMillis());
            inputStreamReader.close();
            inputStream.close();
            socket.close();
            serverSocket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

客户端代码:

public class TestClientDeliverStringToServerClient {

    public static void main(String[] args) {
        try {
            System.out.println("socket begin " + System.currentTimeMillis());
            Socket socket = new Socket("localhost",8088);
            System.out.println("socket end " + System.currentTimeMillis());
            Thread.sleep(3000);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("我是外星人".getBytes());
            outputStream.close();
            socket.close();
        }catch (IOException e){
            e.printStackTrace();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

先运行TestClientDeliverStringToServerServer类,可以看到阻塞在了serverSocket的accept处,然后运行TestClientDeliverStringToServerClient类,可以看到在3秒的线程睡眠结束后,服务端输出了以下结果:

accept begin 1575383118142
accept end 1575383192651
read begin 1575383192651
我是外
星人
read end 1575383195651

服务端向客户端传递字符串

服务端示例代码:

public class TestServerDeliverStringToClientServer {

    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8088);
            System.out.println("server阻塞开始=" + System.currentTimeMillis());
            Socket socket = serverSocket.accept();
            System.out.println("server阻塞结束=" + System.currentTimeMillis());

            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("我来自server端".getBytes());
            outputStream.close();
            socket.close();
            serverSocket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

客户端示例代码:

public class TestServerDeliverStringToClientClient {

    public static void main(String[] args) {
        try {
            System.out.println("client连接准备=" + System.currentTimeMillis());
            Socket socket = new Socket("localhost",8088);
            System.out.println("client连接结束=" +  System.currentTimeMillis());

            char[] charBuffer = new char[3];
            InputStream inputStream = socket.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);

            System.out.println("serverB begin " + System.currentTimeMillis());
            int readLength = inputStreamReader.read(charBuffer);
            System.out.println("serverB end " + System.currentTimeMillis());

            while (readLength != -1){
                System.out.println(new String(charBuffer,0,readLength));
                readLength = inputStreamReader.read(charBuffer);
            }
            System.out.println();
            inputStream.close();
            socket.close();

            System.out.println("client运行结束=" + System.currentTimeMillis());
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

首先运行TestServerDeliverStringToClientServer服务端类,会发现在accept处出现了阻塞。因为client端还没有启动连接上,然后启动TestServerDeliverStringToClientClient客户端类,会发现客户端控制台里边打印了以下内容:

client连接准备=1575384097932
client连接结束=1575384097939
serverB begin 1575384097939
serverB end 1575384097939
我来自
ser
ver
端
    
client运行结束=1575384097940

允许多次调用write()方法进行写入操作

write()方法允许多次被调用,每执行一次就代表传递一次数据。

服务端示例代码:

public class TestMultiWriteServer {

    public static void main(String[] args) {
        try {
            char[] charBuffer = new char[15];
            ServerSocket serverSocket = new ServerSocket(8088);
            System.out.println("server阻塞开始=" + System.currentTimeMillis());
            Socket socket = serverSocket.accept();
            System.out.println("server阻塞结束=" + System.currentTimeMillis());

            InputStream inputStream = socket.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);

            System.out.println("serverB begin " + System.currentTimeMillis());
            int readLength = inputStreamReader.read(charBuffer);
            System.out.println("serverB end " + System.currentTimeMillis());
            while (readLength != -1){
                System.out.println(new String(charBuffer,0,readLength) + " while " + System.currentTimeMillis());
                readLength = inputStreamReader.read(charBuffer);
            }
            inputStream.close();
            socket.close();
            serverSocket.close();
            System.out.println("server端运行结束= "+ System.currentTimeMillis());
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

客户端示例代码:

public class TestMultiWriteClient {

    public static void main(String[] args) {
        try {
            System.out.println("client连接准备=" + System.currentTimeMillis());
            Socket socket = new Socket("localhost",8088);
            System.out.println("client连接结束=" + System.currentTimeMillis());
            Thread.sleep(2000);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write("我是客户端1".getBytes());
            Thread.sleep(3000);
            outputStream.write("我是客户端2".getBytes());
            Thread.sleep(3000);
            outputStream.write("我是客户端3".getBytes());
            Thread.sleep(3000);
            outputStream.write("我是客户端4".getBytes());
            Thread.sleep(3000);
            outputStream.write("我是客户端5".getBytes());
            System.out.println("client close begin=" + System.currentTimeMillis());
            outputStream.close();
            socket.close();
            System.out.println("client close end= " + System.currentTimeMillis());
        }catch (IOException e){
            e.printStackTrace();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

首先运行TestMultiWriteServer服务端类,在accept方法处阻塞,然后运行TestMultiWriteClient客户端代码,服务端控制台每隔三秒钟输出一次客户端新write的内容。

实现服务端与客户端多次的往来通信

实现客户端服务端连续多次的长连接通信。

服务端示例代码:

public class TestDoubleConnServer {

    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8088);
            Socket socket = serverSocket.accept();

            // 输入开始
            InputStream inputStream =socket.getInputStream();
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            int byteLength = objectInputStream.readInt();
            byte[] byteArray = new byte[byteLength];
            objectInputStream.readFully(byteArray);
            String newString = new String(byteArray);
            System.out.println(newString);
            // 输入结束

            // 输出开始
            OutputStream outputStream = socket.getOutputStream();
            String strA = "客户端你好A\n";
            String strB = "客户端你好B\n";
            String strC = "客户端你好C\n";

            int allStrByteLength = (strA + strB + strC).getBytes().length;

            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
            objectOutputStream.writeInt(allStrByteLength);
            objectOutputStream.flush();

            objectOutputStream.write(strA.getBytes());
            objectOutputStream.write(strB.getBytes());
            objectOutputStream.write(strC.getBytes());
            objectOutputStream.flush();
            // 输出结束

            // 输入开始
            byteLength = objectInputStream.readInt();
            byteArray = new byte[byteLength];
            objectInputStream.readFully(byteArray);
            newString = new String(byteArray);
            System.out.println(newString);
            // 输入结束

            // 输出开始
            strA = "客户端你好D\n";
            strB = "客户端你好E\n";
            strC = "客户端你好F\n";

            allStrByteLength = (strA + strB + strC).getBytes().length;

            objectOutputStream.writeInt(allStrByteLength);
            objectOutputStream.flush();

            objectOutputStream.write(strA.getBytes());
            objectOutputStream.write(strB.getBytes());
            objectOutputStream.write(strC.getBytes());
            objectOutputStream.flush();
            // 输出结束

            inputStream.close();
            socket.close();
            serverSocket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

客户端示例代码:

public class TestDoubleConnClient {

    public static void main(String[] args) {
        try {
            Socket socket = new Socket("localhost",8088);
            OutputStream outputStream = socket.getOutputStream();
            InputStream inputStream = socket.getInputStream();

            // 输出开始
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
            String strA = "服务端你好A\n";
            String strB = "服务端你好B\n";
            String strC = "服务端你好C\n";

            int allStrByteLength = (strA + strB + strC).getBytes().length;
            objectOutputStream.writeInt(allStrByteLength);
            objectOutputStream.flush();
            objectOutputStream.write(strA.getBytes());
            objectOutputStream.write(strB.getBytes());
            objectOutputStream.write(strC.getBytes());
            objectOutputStream.flush();
            // 输出结束

            // 输入开始
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            int byteLength = objectInputStream.readInt();
            byte[] byteArray = new byte[byteLength];
            objectInputStream.readFully(byteArray);
            String newString = new String(byteArray);
            System.out.println(newString);
            // 输入结束

            // 输出开始
            strA = "服务端你好D\n";
            strB = "服务端你好E\n";
            strC = "服务端你好F\n";
            allStrByteLength = (strA + strB + strC).getBytes().length;
            objectOutputStream.writeInt(allStrByteLength);
            objectOutputStream.flush();
            objectOutputStream.write(strA.getBytes());
            objectOutputStream.write(strB.getBytes());
            objectOutputStream.write(strC.getBytes());
            objectOutputStream.flush();
            // 输出结束

            // 输入开始
            byteArray = new byte[byteLength];
            objectInputStream.readFully(byteArray);
            newString = new String(byteArray);
            System.out.println(newString);
            // 输入结束

            objectOutputStream.close();
            outputStream.close();
            socket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

readInt处接收不到消息便会阻塞。

可以看到在Java网络编程中,实现长连接通信只要把多个通信内容的处理放入一个通信请求中即可。

先启动服务端,再启动客户端,可以看到各自的控制台上打印出对应的由对方发送过来的信息。

调用Stream的close()方法造成相关联的socket关闭

类型为InputStream的对象inputStream的真正数据类型是SocketInputStream,其close方法源代码如下:

public void close() throws IOException{
        if(closing)
            return;
        closing = true;
        if(socket != null){
            if(!socket.isClosed())
                socket.close(); // 此行代码将会被执行,将socket关闭
        }else{
            impl.close();
        }
        closing = false;
}

从源代码可知,当调用SocketInputStream类的close方法时,顺便也会关闭Socket(套接字)的连接。如果Socket关闭,则服务端与客户端不能进行通信。因此,当执行代码OutputStream outputStream = socket.getOutputStream()取得输出流时,就会出现异常。

使用Socket传递PNG图片文件

服务端示例代码为:

public class TestDeliverPhotoServer {

    public static void main(String[] args) {
        try {
            byte[] byteArray = new byte[2048];
            ServerSocket serverSocket = new ServerSocket(8088);
            Socket socket = serverSocket.accept();

            InputStream inputStream = socket.getInputStream();
            int readLength = inputStream.read(byteArray);

            FileOutputStream pngOutputStream = new FileOutputStream(new File("c:\\abc\\new.png"));

            while (readLength != -1){
                pngOutputStream.write(byteArray,0,readLength);
                readLength = inputStream.read(byteArray);
            }
            pngOutputStream.close();
            inputStream.close();
            socket.close();
            serverSocket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

客户端示例代码为:

public class TestDeliverPhotoClient {

    public static void main(String[] args) {
        try {
            String pngFile = "c:\\abc\\old.png";
            FileInputStream pngStream = new FileInputStream(new File(pngFile));
            byte[] byteArray = new byte[2048];

            System.out.println("socket begin " + System.currentTimeMillis());
            Socket socket =  new Socket("localhost",8088);
            System.out.println("socket end "+ System.currentTimeMillis());

            OutputStream outputStream = socket.getOutputStream();

            int readLength = pngStream.read(byteArray);
            while (readLength != -1){
                outputStream.write(byteArray,0,readLength);
                readLength = pngStream.read(byteArray);
            }
            outputStream.close();
            pngStream.close();
            socket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

首先运行TestDeliverPhotoServer服务端,再运行TestDeliverPhotoClient客户端,在c:\abc目录下创建出了一份新的名为new.png的图片。

TCP连接的三次握手过程

第一次握手:
客户端向服务端发送SYN标志位,目的是与服务端建立连接。SYN标志位的值表示发送数据流序号sequence number的最大值。例如,Seq的值
是5,说明在数据流中曾经一共发送了1,2,3,4这四个字节。而在本次“握手”中,Seq的值是0,代表发送数据流的大小是0。这次握手,客户端
仅仅发送一个SYN标志位到服务端,代表要进行连接。
第二次握手:
服务端向客户端发送SYN和ACK标志位,其中ACK标志位表示是对收到的数据包的确认,说明服务端接收到了客户端的连接。ACK的值是1,表示服务
端期待下一次从客户端发送数据流的序列号是1,而Seq=0代表服务端曾经并没有给客户端发送数据,而本次握手也没有发送数据。
第三次握手:
这次握手时,客户端向服务端发送的ACK标志位为1,Seq的值是1。Seq=1代表这正是服务端所期望的Ack=1。虽然Seq=1,但客户端这次还是没有向服务端传递数据。
而客户端向服务端发送ACK标志位为1的信息,说明客户端期待服务端下一次传送到Seq的值是1。

标志位SYN与ACK值的自增特性

TCP的报文到达确认(ACK),是对接收到的数据的最高序列号的确认,并向发送端返回一个下次接收时期望的TCP数据包的序列号(Ack Number)。例如,主机A发送的当前数据序号是400,数据长度是100,则接收端接收到后会返回一个500的确认号给主机A。

当客户端第一次向服务端传输数据时,客户端发送标志位PSH和ACK。标志位PSH的作用是发送数据,让接收方立即处理数据。

TCP断开连接的4次“挥手”过程

四次挥手的过程如下:
1、客户端到服务端:我关了
2、服务端到客户端:好的,收到
3、服务端到客户端:我也关了
4、客户端到服务端:好的,收到

结合多线程Thread实现通信

在Socket技术中,常见的实践方式就是Socket结合Thread多线程技术,客户端每发起一次新的请求,就把这个请求交给新创建的线程来执行这次任务。

先是不使用线程池的方式:

不使用线程池的服务端代码:

public class BeginServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8888);
        int runTag = 1;
        while (runTag ==1 ){
            Socket socket = serverSocket.accept();
            BeginThread beginThread = new BeginThread(socket);
            beginThread.start();
        }
        serverSocket.close();
    }
}

不使用线程池的客户端代码:

public class BeginClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("localhost",8888);
        OutputStream outputStream  = socket.getOutputStream();
        outputStream.write("我是中国人".getBytes());
        outputStream.close();
        socket.close();
    }
}

不使用线程池的处理线程:

public class BeginThread extends Thread{

    private Socket socket;

    public BeginThread(Socket socket){
        super();
        this.socket = socket;
    }

    @Override
    public void run() {
        try{
            InputStream inputStream = socket.getInputStream();
            InputStreamReader reader = new InputStreamReader(inputStream);
            char[] charArray = new char[1000];
            int readLength = -1;
            while ((readLength = reader.read(charArray)) != -1){
                String newString = new String(charArray,0,readLength);
                System.out.println(newString);
            }
            reader.close();
            inputStream.close();
            socket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

在上述程序运行后,服务端与客户端成功地进行通信,每个任务以异步的方式一起执行,大大增加吞吐量,提高了数据处理的能力。

然后是使用线程池的示例代码:

使用线程池的处理线程代码:

public class ReadRunnable implements Runnable {

    private Socket socket;

    public ReadRunnable(Socket socket){
        super();
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            InputStream inputStream = socket.getInputStream();
            byte[] byteArray = new byte[100];
            int readLength = inputStream.read(byteArray);
            while (readLength != -1){
                System.out.println(new String(byteArray,0,readLength));
                readLength = inputStream.read(byteArray);
            }
            inputStream.close();
            socket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

使用线程池的服务端代码:

public class ThreadServer {

    private ServerSocket serverSocket;
    private Executor pool;

    public ThreadServer(int port,int poolSize){
        try {
            serverSocket = new ServerSocket(port);
            pool = Executors.newFixedThreadPool(poolSize);
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public void startService(){
        try{
            for (;;){
                Socket socket = serverSocket.accept();
                pool.execute(new ReadRunnable(socket));
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception{
        ThreadServer server = new ThreadServer(8088,10000);
        server.startService();
    }
}

服务端与客户端互传对象以及I/O流顺序问题

实现Server服务端与Client交换Userinfo对象,而不是String类型的数据。

对应的UserInfo类:

public class UserInfo implements Serializable {

    private long id;

    private String username;

    private String password;

    public UserInfo(){

    }

    public UserInfo(long id,String username,String password){
        super();
        this.id = id;
        this.username = username;
        this.password = password;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

测试传输对象的服务端代码:

public class TestDeliverModelServer {

    public static void main(String[] args) throws IOException,ClassNotFoundException {
        ServerSocket serverSocket = new ServerSocket(8888);
        Socket socket = serverSocket.accept();
        InputStream inputStream = socket.getInputStream();
        OutputStream outputStream = socket.getOutputStream();

        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        for (int i = 0; i < 5; i++) {
            UserInfo userInfo = (UserInfo)objectInputStream.readObject();
            System.out.println("在服务端打印" + (i + 1) + ": " + userInfo.getId()
                                + " "+userInfo.getUsername() + " "
                                + userInfo.getPassword());
            UserInfo newUserinfo = new UserInfo();
            newUserinfo.setId(i + 1);
            newUserinfo.setUsername("serverUsername" + (i+1));
            newUserinfo.setPassword("serverPassword" + (i+1));

            objectOutputStream.writeObject(newUserinfo);
        }
        objectOutputStream.close();
        objectInputStream.close();

        outputStream.close();
        inputStream.close();

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

测试传输对象的客户端代码:

public class TestDeliverModelClient {

    public static void main(String[] args) throws IOException,ClassNotFoundException {
        Socket socket  = new Socket("localhost",8888);
        InputStream inputStream = socket.getInputStream();
        OutputStream outputStream = socket.getOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        for (int i = 0; i < 5; i++) {
            UserInfo newUserinfo = new UserInfo();
            newUserinfo.setId(i + 1);
            newUserinfo.setUsername("clientUsername" + (i + 1));
            newUserinfo.setPassword("clientPassword" + (1 + 1));

            objectOutputStream.writeObject(newUserinfo);

            UserInfo userInfo = (UserInfo) objectInputStream.readObject();
            System.out.println("在客户端打印" + (i + 1) + ": " + userInfo.getId() +
                                " " + userInfo.getUsername() + " "
                                + userInfo.getPassword());
        }
        objectOutputStream.close();
        objectInputStream.close();

        outputStream.close();
        inputStream.close();

        socket.close();
    }
}

运行后发现服务端控制台和客户端控制台成功打印出了对方传过来的UserInfo对象的字段信息。

另外,由于accept方法会阻塞,所以:

  1. 服务端先获得ObjectInputStream对象,客户端就要先获得ObjectOutputStream对象
  2. 服务端先获得ObjectOutputStream对象,客户端就要先获得ObjectInputStream对象

ServerSocket类的使用

接受accept与超时Timeout

public Socket accept()方法的作用就是侦听并接受此套接字的连接。此方法在连接传入之前一直阻塞。

setSoTimeout(timeout)方法的作用是设置超时时间,通过指定超时timeout启用/禁用SO_TIMEOUT,以ms为单位。设置后,accept()方法将只阻塞timeout的时间长度。如果超过超时值,将引发java.net.SocketTimeoutException,但ServerSocket仍旧有效,在结合try-catch结构后,还可以继续进行accept()方法的操作。int getSoTimeout()方法的作用是获取SO_TIMEOUT的设置。

验证超时的服务端示例代码:

public class TestTimeoutServer {


    public static void main(String[] args) {
        try{
            ServerSocket serverSocket = new ServerSocket(8000);
            System.out.println(serverSocket.getSoTimeout());
            serverSocket.setSoTimeout(4000);
            System.out.println(serverSocket.getSoTimeout());
            System.out.println();

            System.out.println("begin " + System.currentTimeMillis());
            serverSocket.accept();
            System.out.println(" end " + System.currentTimeMillis());
        }catch (IOException e){
            e.printStackTrace();
            System.out.println("catch " + System.currentTimeMillis());
        }
    }
}

验证超时的客户端示例代码:

public class TestTimeoutClient {

    public static void main(String[] args) {
        try {
            System.out.println("client begin " + System.currentTimeMillis());
            Socket socket = new Socket("localhost",8000);
            System.out.println("client end " + System.currentTimeMillis());
        }catch (IOException e){
            e.printStackTrace();
            System.out.println("catch " + System.currentTimeMillis());
        }
    }
}

启动TestTimeoutServer类后等待4秒,控制台报SocketTimeoutException的错。

构造方法的backlog参数含义

ServerSocket类的构造方法 public ServerSocket(int port,int backlog) 中的参数backlog的主要作用是允许接受客户端连接请求的个数。客户端有很多连接进入到操作系统中,将这些连接放入操作系统的队列中,当执行accept()方法时,允许客户端连接的个数要取决于backlog参数。传入backlog参数的作用是设置最大等待队列长度,如果队列已满,则拒绝该连接。默认值为50.

服务端示例代码:

public class TestBacklogServer {

    public static void main(String[] args) throws IOException,InterruptedException {
        ServerSocket serverSocket = new ServerSocket(8088,3);
        // sleep(5000)的作用是不让ServerSocket调用accept()方法
        // 而是由客户端Socket先发起10个连接请求
        // 然后在执行accept()方法时只能接受3个连接
        Thread.sleep(5000);

        System.out.println("accept1 begin");
        Socket socket1 = serverSocket.accept();
        System.out.println("accept1 end");

        System.out.println("accept2 begin");
        Socket socket2 = serverSocket.accept();
        System.out.println("accept2 end");

        System.out.println("accept3 begin");
        Socket socket3 = serverSocket.accept();
        System.out.println("accept3 end");

        System.out.println("accept4 begin");
        Socket socket4 = serverSocket.accept();
        System.out.println("accept4 end");

        System.out.println("accept5 begin");
        Socket socket5 = serverSocket.accept();
        System.out.println("accept5 end");

        socket1.close();
        socket2.close();
        socket3.close();
        socket4.close();
        socket5.close();

        serverSocket.close();
    }
}

客户端示例代码:

public class TestBacklogClient {

    public static void main(String[] args)throws IOException,InterruptedException {
        Socket socket1 = new Socket("localhost",8088);
        Socket socket2 = new Socket("localhost",8088);
        Socket socket3 = new Socket("localhost",8088);
        Socket socket4 = new Socket("localhost",8088);
        Socket socket5 = new Socket("localhost",8088);
    }
}

首先允许TestBacklogServer类,然后以最快的速度允许TestBacklogClient类。可以发现在TestBacklogClient第四次发送的时候报错了。因为TestBacklogServer设置了backlog的值为3。

Socket类的使用

Socket类的主要作用是使Server与Client进行通信。

绑定bind与connect以及端口生成的时机

public void bind(SocketAddress bindpoint)方法的作用是将套接字绑定到本地地址。在Socket通信的过程中,服务端和客户端都需要端口来进行通信。在前面的代码中,使用代码"new Socket("localhost",8888)"来创建客户端的Socket并连接服务端的8888端口,客户端的端口并没有指定,而是采用自动分配端口号的算法。也可以使用bind()方法在客户端的Socket中指定使用某个具体的端口。bind()方法就是将客户端绑定到指定的端口上,该方法要优先于connect()方法执行,也就是先绑定本地端口再执行连接方法。

public void connect(SocketAddress endpoint)方法的作用就是将此套接字连接到服务端。

服务端示例代码:

public class TestBindConnectServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8888);
        Socket socket = serverSocket.accept();
        socket.close();
        serverSocket.close();
        System.out.println("server end!");
    }
}

客户端示例代码:

public class TestBindConnectClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket();
        socket.bind(new InetSocketAddress("localhost",7777));
        socket.connect(new InetSocketAddress("localhost",8888));
        socket.close();
        System.out.println("client end!");
    }
}

如果先使用了系统默认分配客户端端口的方式connect()了之后,又显式执行bind()方法,则会出现Aready bound异常。

连接与超时

public void connect(SocketAddress endpoint,int timeout)方法的作用是将此套接字连接到服务端,并指定一个超时值。在windows系统中,默认的超时时间是20s。若超过timeout还没有连接到服务端,则出现异常。

获得远程端口与本地端口

public int getPort()方法的作用是返回此套接字连接到的远程端口。

public int getLocalPort()方法的作用是返回此套接字绑定到的本地端口。

获取本地ip地址和端口

public InetAddress getLocalAddress():获取套接字绑定的本地InetAddress地址,调用获取到的InetAddress的getAddress()方法可以获得相应的本地ip地址。

public SocketAddress getLocalSocketAddress():返回套接字绑定的InetSocketAddress,进而调用InetSocketAddress的getPort()方法可以获得本地的端口。

获得远程ip和端口

public InetAddress getInetAddress():返回此套接字连接到的远程的InetAddress地址。如果套接字是未连接的,返回null。进而调用返回的InetAddress的getAddress()方法,可以获得远程的ip。

public SocketAddress getRemoteSocketAddress():返回此套接字远程端点的SocketAddress地址,如果未连接,则返回null。进而调用返回的SocketAddress的getPort()方法可以获取远程的端口。

套接字状态的判断

public boolean isBound()方法的作用是返回套接字的绑定状态。如果将套接字成功地绑定到一个地址,则返回true。

public boolean isConnected()方法的作用是返回套接字的连接状态。如果将套接字成功地连接到服务端,则为true。

public boolean isClosed()的作用是返回套接字的关闭状态。如果已经关闭了套接字。则返回true。

public synchronized void close()的作用是关闭此套接字。所有当前阻塞于此套接字上的I/O操作中的线程都将抛出SocketException。套接字被关闭后,便无法重新连接或重新绑定,如果想再次使用套接字,则需要创建新的套接字。

关闭套接字也将会关闭该套接字的InputStream和OutputStream。如果套接字有一个与之关联的通道,则关闭该通道。

关闭输入输出流

public boolean isInputShutdown():判断输入流是否关闭

public boolean isOutputShutdown():判断输出流是否关闭

Socket选项TcpNoDelay

public void setTcpNoDelay(boolean on):启用/禁用TCP_NODELAY。

public boolean getTcpNoDelay():测试是否已启用TCP_NODELAY。

设置TCP_NODELAY为true可以使数据立即发送,而不是等到达到一定规模一起发送,这样可以提高实时性,但是会使网络连接次数增加。

Socket选项SendBufferSize

Socket中的SO_LINGER选项用来控制Socket关闭close()方法时的行为。在默认情况下,执行close()方法后,该方法会立即返回,但底层的Socket并不会立即关闭,它会延迟一段时间。在延迟的时间里将“发送缓冲区”中的剩余数据在延迟的时间内继续发送给对方,然后才会真正地关闭Socket连接。

public void setSoLinger(boolean on,int linger):启用/禁用执行完close()方法后Socket的逗留行为。第二个参数是逗留时间。

Socket选项Timeout

setSoTimeout(int timeout):启用/禁用带有指定超时值的SO_TIMEOUT,以毫秒为单位。将此选项设为非0的超时值时,在与此Socket关联的InputStream上调用read()方法将只阻塞此时间长度。如果超过超时值,将引发SocketTimeoutException异常,尽管Socket仍旧有效。启用timeOut特性必须在进入阻塞操作前被启用才能生效。

public int getSoTimeout()方法的作用是返回SO_TIMEOUT的设置。

Socket选项OOBInline

Socket的选项SO_OOBINLINE的作用是在套接字上接收的所有TCP紧急数据都将通过套接字输入流接收。禁用该选项时(默认),将悄悄丢弃紧急数据。OOB可以理解成是需要紧急发送的数据。可通过setOOBInline()的方法启用/禁用OOBInline选项。默认情况下是禁用的,即在套接字上接收的TCP紧急数据被静默丢弃。如果需要接收紧急数据,则必须启用此选项。public void sendUrgentData(int data)方法在接收端进行设置来决定是否接收紧急数据,在发送端,使用public void sendUrgentData(int data)方法发送紧急数据,这个方法向对方发送1个单字节的数据,但是这个单字节的数据并不存储在输出缓冲区中,而是立即将数据发送出去,而在对方程序中并不知道发送过来的数据是由OutputStream还是由sendUrgentData(int data)发送过来的。使用sendUrgentData()方法发送的数据比使用write()方法要优先紧急发送。调用write()方法写入的数据其实是放入缓冲区中的,直到执行flush()方法才发送。

在调用sendUrgentData()方法时所发送的数据可以被对方所忽略,结合这个特性可以实现测试网络连接状态的心跳机制,示例代码:

心跳测试服务端代码:

public class TestHeartBeatServer {


    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8888);
            Socket socket = serverSocket.accept();
            Thread.sleep(Integer.MAX_VALUE);
            socket.close();
            serverSocket.close();
        }catch (SocketException e){
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

心跳测试客户端代码:

public class TestHeartBeatClient {

    public static void main(String[] args) throws Exception{
        Socket socket = new Socket("127.0.0.1",8888);
        try {
            int count = 0;
            for (;;){
                socket.sendUrgentData(1);
                count++;
                System.out.println("执行了 " + count + "次嗅探");
            }
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("----网络断开了");
            socket.close();
        }
    }
}