Socket的使用

帅比萌擦擦* 提交于 2020-01-22 04:00:33

什么是Socket

Socket也称为"套接字",是网络通信中的概念,是支持TCP/IP协议的网络通信的基本操作单元。它分为流式套接字和用户数据报套接字两种,分别对应于传输层的TCP和UDP协议。TCP协议是一种面向连接的、可靠的、基于字节流的传输层协议,由IETF的RFC793定义。

Socket包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

Socket的初始化方式

Socket的初始化用在客户端中,这里介绍几种初始化方式

1.无代理模式创建

//等效于空构造函数
Socket socket = new Socket(Proxy.NO_PROXY); 

2.使用HTTP代理的方式

// 新建一份具有HTTP代理的套接字,传输数据将通过www.baidu.com:8080端口转发
Proxy proxy = new Proxy(Proxy.Type.HTTP,new InetSocketAddress(Inet4Address.getByName("www.baidu.com"), 8800));
Socket  socket = new Socket(proxy);

3.直接指定服务端ip和端口的方式创建

//创建socket并连接到192.168.1.5:20000 服务端
Socket  socket = new Socket(Inet4Address.getByName("192.168.1.5"), 20000);
//或者,参数1是host,可以是域名或者ip,如果是本机可以是localhost
Socket  socket = new Socket("192.168.1.5", 20000); 

4.指定服务端ip和端口以及客户端ip和端口的方式创建
这种方式创建的Socket会直接连接到服务端,并且客户端也会绑定到特定的端口上。

//参数1,2表示服务端的host和端口, 参数3,4表示客户端address和端口
Socket  socket = new Socket("192.168.1.5", 20000, Inet4Address.getLocalHost(), 30000);
//或者,参数1,2表示服务端的address和端口, 参数3,4表示客户端address和端口
Socket  socket = new Socket(Inet4Address.getByName("192.168.1.5"), 20000, Inet4Address.getLocalHost(), 30000);

5.使用空构造函数
建议使用该方式,因为空参方式创建的Socket不会立马连接到服务端,这样客户端就可以配置一些额外的参数,否则连接已建立的话,就无法设置参数了(设置了也会被忽略)。

Socket socket = new Socket();
//通过bind方法绑定客户端的address和端口
socket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), 30000));

Socket参数配置

通过上面1,2,5方式创建的Socket并未真正连接到服务端,因此可以在连接前设置一些额外的参数,通过各种set方法来设置,例如:

方法名 含义
setSoTimeout(int timeout) 设置读取超时时间,单位毫秒
setReuseAddress(boolean on) 是否复用未完全关闭的Socket地址,默认false。 设置为true,在Linux平台上的表现是如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用 端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息, 抛出“Address already in use: JVM_Bind”。如果你的服务程序停止后想立即重启,不等60秒,而新套接字依旧 使用同一端口,此时 SO_REUSEADDR 选项非常有用。在Windows平台上,多个Socket新建立对象可以绑定在同一个端口上,不用判断TCP的状态。使用SO_REUSEADDR选项时有两点需要注意:1.必须在调用bind方法之前使用setReuseAddress方法来打开SO_REUSEADDR选项,因此,要想使用SO_REUSEADDR选项,就不能通过Socket类的构造方法来绑定端口,2. 必须将绑定同一个端口的所有的Socket对象的SO_REUSEADDR选项都打开才能起作用。
setTcpNoDelay(boolean on) 是否开启Nagle算法,默认是启用的,设置true则关闭,Nagle算法用于避免网络中充塞小封包,提高网络的利用率,但是如果是write-write-read这种应用编程上建议关闭,原因可以查看java socket参数详解:TcpNoDelay
setKeepAlive(boolean on) 是否需要在长时无数据响应时发送确认数据(类似心跳包),时间大约为2小时
setSoLinger(boolean on, int linger) 对于close关闭操作行为进行怎样的处理;接收2个参数,默认为false,0,默认的行为是关闭时立即返回,并由底层系统接管输出流,将缓冲区内的数据发送完毕才关闭连接;若是true、0:关闭时立即返回,缓冲区数据会被丢弃,直接发送RST结束命令到对方,并无需经过2MSL等待;true、200:关闭时最长阻塞200毫秒,如果在200毫秒内数据发送完毕,则通过四次挥手的方式正常关闭连接,否则通过发送RST包的方式强行关闭连接。更多详情可以查看这篇文章细说Java Socket中的setSoLinger方法
setOOBInline(boolean on) 是否让紧急数据内敛,默认false;紧急数据通过 socket.sendUrgentData(低8位int值)来发送,如果要启用的话,客户端和服务端都必须设置为true才有用,如果这个Socket选项打开,可以通过Socket类的sendUrgentData方法向服务器发送一个单字节的数据。这个单字节数据并不经过输出缓冲区,而是立即发出。虽然在客户端并不是使用OutputStream向服务器发送数据,但在服务端程序中这个单字节的数据是和其它的普通数据混在一起的。因此,在服务端程序中并不知道由客户端发过来的数据是由OutputStream还是由sendUrgentData发过来的。
setReceiveBufferSize(int size) 设置接收缓冲器大小,单位byte,默认接收缓冲区大小是8096个字节(8K)。这个值是Java所建议的输入缓冲区的大小。如果这个默认值不能满足要求,可以用setReceiveBufferSize方法来重新设置缓冲区的大小。但最好不要将输入缓冲区设得太小,否则会导致传输数据过于频繁,从而降低网络传输的效率;如果底层的Socket实现不支持SO_RCVBUF选项,这个方法将会抛出SocketException例外。必须将size设为正整数,否则setReceiveBufferSize方法将抛出IllegalArgumentException异常。另外,如果你增加了发送缓存区大小,而对方却没有增加它对应的接收缓冲大小,那么在TCP三握手时,最后确定的最大发送窗口还是双方最小的那个缓冲区,发了更多的数据,那么多出来的数据也会被丢弃。除非双方都协商好。
setSendBufferSize(int size) 设置发送缓冲器大小,单位byte,默认发送缓冲区大小是8096个字节(8K),参考setReceiveBufferSize
setPerformancePreferences(int connectionTime, int latency, int bandwidth) 用于设置性能参数:短链接(connectionTime),延迟(latency),带宽(bandwidth)3者之间的重要性权重,权重值都是int类型,数值越高,重要性越高。

Socket核心方法

方法名 含义
connect(SocketAddress host,int timeout) 连接到服务端,接收2个参数,InetSocketAddress和timeout,前者是对服务端IP和端口的封装,timeout是连接超时,超时会抛异常
InetAddress getInetAddress() 返回套接字连接的服务端地址。
int getPort() 返回此套接字连接到的服务端端口
int getLocalPort() 返回此套接字绑定到的本地端口
InetAddress getLocalAddress() 返回此套接字的客户端address
SocketAddress getRemoteSocketAddress() 返回此套接字连接的服务端的地址和端口信息,如果未连接则返回null
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流
close() 关闭并释放资源,只要服务端收到了客户端的关闭信号,就立即关闭连接。
shutdownInput() 此套接字的输入流至于"流的末尾",就是说流已经读到了末尾,再没有数据可以读了,还会将以后发过来的数据忽略掉。注意此方法并不关闭网络连接。
shutdownOutput 禁用此套接字的输出流,通常在写完数据的时候调用此方法添加结束标记,以便对方的read方法能够读到-1或者readLine方法能够读到null,而不至于让对方一直阻塞在读操作中
isInputShutdown 判断输入流是否被关闭
isOutputShutdown 判断输出流是否被关闭
isClosed 判断socket是否已关闭

ServerSocket的初始化方式

ServerSocket的初始化用在服务端中,这里介绍几种初始化方式

1.无参构造方法

//需要通过bind方法来监听端口
ServerSocket serverSocket = new ServerSocket();

2.指定监听端口的方式

//创建ServerSocket并监听20000端口,同时默认设置当前可允许等待链接的队列为50个
ServerSocket serverSocket = new ServerSocket(20000)

3.指定监听端口并设置等待连接的队列大小

//监听20000端口,等待队列大小为100个
ServerSocket serverSocket = new ServerSocket(20000, 100, Inet4Address.getLocalHost());
//或者
ServerSocket serverSocket = new ServerSocket(20000, 50);

上面2,3方式创建的ServerSocket会设置socket等待连接的队列大小,即backlog参数,默认是50个,也就是说ServerSocket有一个队列,存放还没有来得及处理的客户端Socket,如果队列已经被客户端socket占满了,并且还有新的连接过来,那么ServerSocket会拒绝新的连接,抛出refused connect的异常。

另外,通过方式1创建的ServerSocket,需要调用bind方法来监听端口,例如:

 serverSocket.bind(new InetSocketAddress(Inet4Address.getLocalHost(), 20000), 100);

ServerSocket常用方法

方法名 含义
int getLocalPort() 返回此套接字在服务端侦听的端口,或者说服务器绑定的端口
Socket accept() 侦听并接受到此套接字的连接
void setSoTimeout(int timeout) 通过指定超时值启用/禁用SO_TIMEOUT,以毫秒为单位
void bind(SocketAddress host,int backlog) 将ServerSocket绑定到特定地址(IP地址和端口号)并设置等待队列大小
InetAddress getInetAddress() 返回此套接字的客户端address
boolean isBound() 判断ServerSocket是否已经与一个端口绑定,只要ServerSocket已经与一个端口绑定,即使它已经被关闭,isBound()方法也会返回true
close() 服务器释放占用的端口,并且断开与所有客户的连接
boolean isClosed() 判断ServerSocket是否关闭,只有执行了ServerSocket的close()方法,isClosed()方法才返回true;否则,即使ServerSocket还没有和特定端口绑定,isClosed()方法也会返回false

示例

先来写个客户端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.*;

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) throws IOException {
        System.out.println("Client start");

        //创建Socket
        Socket socket = new Socket();
        //设置socket的相关参数
        initSocket(socket);
        //开始会话
        new Conversation(socket).start();

    }

    /**
     * 配置Socket参数
     *
     * @param socket
     * @throws IOException
     */
    private static void initSocket(Socket socket) throws IOException {
        //设置读取超时时间5秒
        socket.setSoTimeout(5000);
        //设置保持长连接
        socket.setKeepAlive(true);
        //设置可以复用未完全关闭的Socket地址
        socket.setReuseAddress(true);
        //设置接收缓冲区大小为64kb
        socket.setReceiveBufferSize(64 * 1024);
        //设置发送缓冲区大小为64kb
        socket.setSendBufferSize(64 * 1024);
        //关闭Nagle算法,关闭延迟ack
        socket.setTcpNoDelay(true);
        //设置关闭操作最长延迟200毫秒,超时未发送完的数据会丢弃并强行关闭连接,否则正常关闭
        socket.setSoLinger(true, 200);
        //设置支持发送紧急数据
        socket.setOOBInline(true);
        //设置连接/延迟/带宽的权重
        socket.setPerformancePreferences(1, 1, 0);
        //绑定客户端的具体ip和端口,当客户端有多个网卡或者多个ip时可以这样指定具体的ip
        socket.bind(new InetSocketAddress(Inet4Address.getByName("192.168.1.5"), 30000));
    }

    /**
     * 会话线程
     */
    private static class Conversation extends Thread {
        private Socket socket;
        private boolean isClosed = false;

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

        @Override
        public void run() {
            super.run();
            try {
                //连接到服务端
                socket.connect(new InetSocketAddress(InetAddress.getByName("192.168.1.5"), 20000));

                System.out.println("client ip:" + socket.getLocalAddress().getHostAddress() + " port:" + socket.getLocalPort());
                System.out.println("server ip:" + socket.getInetAddress().getHostAddress() + " port:" + socket.getPort());

                //获取键盘输入流构建缓冲读取流
                BufferedReader bufReader = new BufferedReader(new InputStreamReader(System.in));
                //获取socket输出流构建打印流
                PrintWriter socketPrintWriter = new PrintWriter(socket.getOutputStream(), true);
                //获取socket输入流并构建缓冲读取流
                BufferedReader socketBufReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                //循环读写操作
                do {
                    //读取键盘输入的一行
                    String inputStr = bufReader.readLine();
                    //给服务端写数据
                    socketPrintWriter.println(inputStr);
                    //读取服务端返回的一行数据
                    String response = socketBufReader.readLine();
                    //将服务端数据显示在控制台
                    System.out.println("server:" + response);
                    if ("bye".equalsIgnoreCase(response)) {
                        isClosed = true;
                    }

                } while (!isClosed);
                //关闭流
                bufReader.close();
                socketPrintWriter.close();

            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("连接异常:" + e.getMessage());
            } finally {
                try {
                    System.out.println("conversation finished server ip:" + socket.getInetAddress().getHostAddress() 
                            + " port:" + socket.getPort());
                    socket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("Client closed");
            }
        }
    }
}

然后写个服务端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.*;

/**
 * 服务端
 */
public class Server {

    public static void main(String[] args) throws IOException {
        System.out.println("Server start");

        //创建服务端serverSocket
        ServerSocket serverSocket = new ServerSocket();
        //初始化serverSocket
        initServerSocket(serverSocket);

        // 等待客户端连接
        for (; ; ) {
            Socket socket = serverSocket.accept();
            //单独启动一个线程去处理客户端的接入
            new ClientHandler(socket).start();
        }

    }

    /**
     * 创建服务端serverSocket
     *
     * @param serverSocket
     * @throws IOException
     */
    private static void initServerSocket(ServerSocket serverSocket) throws IOException {
        //设置accept的超时时间,不设置表示永久等待
        //serverSocket.setSoTimeout(5000);
        //设置接收缓冲区大小64k,保持和客户端的写入缓冲区大小一致
        serverSocket.setReceiveBufferSize(64 * 1024);
        //重用端口
        serverSocket.setReuseAddress(true);
        //设置连接/延迟/带宽的权重
        serverSocket.setPerformancePreferences(1, 1, 0);
        //绑定本地地址和监听端口,同时设置队列的等待大小为100个
        serverSocket.bind(new InetSocketAddress(Inet4Address.getByName("192.168.1.5"), 20000), 100);
    }

    private static class ClientHandler extends Thread {
        private Socket socket;
        private boolean isClosed;

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

        @Override
        public void run() {
            super.run();
            System.out.println("client connected ip:" + socket.getInetAddress().getHostAddress()
                    + " port:" + socket.getPort());

            try {
                //获取socket输入流,构建缓冲读取流
                BufferedReader socketBufReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                //获取socket输出流,构建打印流
                PrintWriter socketPrintWriter = new PrintWriter(socket.getOutputStream(), true);

                do {
                    //读取客户端一行数据
                    String clientMsg = socketBufReader.readLine();
                    //显示在控制台
                    System.out.println("client:" + clientMsg);
                    if ("bye".equalsIgnoreCase(clientMsg)) {
                        isClosed = true;
                        //回送bye
                        socketPrintWriter.println("bye");
                    } else {
                        //回送读取的长度
                        socketPrintWriter.println(clientMsg.length());
                    }
                } while (!isClosed);

                //关闭流
                socketBufReader.close();
                socketPrintWriter.close();

            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                // 连接关闭
                try {
                    System.out.println("client disconnected ip:" + socket.getInetAddress().getHostAddress() 
                            + " port:" + socket.getPort());
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

先运行服务端,然后在运行客户端
在这里插入图片描述
然后客户端这边键盘输入内容,服务端返回内容长度,输入bye后断开客户端连接
在这里插入图片描述

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!