JAVA网络编程-客户端Socket

 使用Socket

用Socket写入服务器
构造和连接Socket
Socket地址
代理服务器
获取Socket的信息
关闭还是连接
Socket选项
Socket异常

使用Socket

socket是两台主机之间的一个连接。它可以完成7个基本操作:

连接远程机器;发送数据;接收数据;关闭连接;绑定端口;监听入站数据;在绑定端口上接收来自远程机器的连接。

前4个步骤对应的4个操作方法应用于客户端(Socket),后面三个操作仅服务器需要(ServerSocket)

一旦建立了连接,本地和远程主机就从这个socket得到输入流和输出流,使用者两个流相互发送数据。连接是全双工的,两台主机都可以同时发送和接收数据。数据的含义取决与协议,发送给FTP服务器的命令与发送给HTTP服务器的命令有所不同。一般先完成某种协议握手,然后再具体传输数据。

当数据传输结束后,一端或两端将关闭连接。有些协议,如HTTP1.0要求每次请求得到服务器后都要关闭连接。而FTP或者HTTP1.1则允许在一个连接上处理多个请求。

 

public static void main(String[] args) throws Exception {
        try (Socket socket = new Socket("127.0.0.1", 8888)) {
        } catch (Exception e) {
            System.out.println(e);
        }
    }// 客户端

使用setSoTimeout(int m)方法为连接设置一个超时时间,超时时间的单位是毫秒。

一旦打开Socket并设置其超时时间后,可以调用getInputStream()返回一个InputStream,用它从socket中读取子节。一般来讲,服务器可以发送任意子节。确认读取完毕后调用shutdownInput()方法关闭输入流。

public static void main(String[] args) throws Exception {
        try (Socket socket = new Socket("127.0.0.1", 8888)) {
            socket.setSoTimeout(1000);
            InputStream in = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String line = null;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }

socket.shutdownInput();
} catch (Exception e) { System.out.println(e); } }// 客户端

用Socket写入服务器

getOutputStream();返回一个原始的OutputStream,可以用它从你的应用向Socket的另一端写数据。确认写入完毕后调用shutdownOutput();

@RequestMapping(value = "test")
    public String g(HttpServletRequest request) throws IOException {
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            Enumeration<String> headers = request.getHeaders(key);
            while (headers.hasMoreElements()) {
                String value = headers.nextElement();
                System.out.println(key + "   " + value);
            }

        }
        return "success1";
    }// 服务端
public static void main(String[] args) throws Exception {
        try (Socket socket = new Socket("127.0.0.1", 8888)) {
            
            OutputStream out = socket.getOutputStream();
            BufferedWriter bw  = new BufferedWriter(new OutputStreamWriter(out));
            bw.append("GET /test HTTP/1.1\r\n");
            bw.append("Host: 127.0.0.1:8888\r\n");
            bw.append("\r\n");
            
            bw.flush();
            socket.shutdownOutput();
            
            
            InputStream in = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String line = null;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
            socket.shutdownInput();
        } catch (Exception e) {
            System.out.println(e);
        }
    }// 客户端

构造和连接Socket

java.net.Socket类是Java完成客户端TCP操作的基础类,其他建立TCP网络连接的面向客户端的类如URL,RULConnection最终都会调用这个类的方法,这个类本身使用原生代码与主机操作系统的本地TCP栈进行通信。

基本的构造函数

以下两个构造函数会在构造后立刻与主机建立连接。如果出于某种原因未能打开连接,构造函数会抛出一个IOException或UnknownHostException异常。

public static void main(String[] args) throws Exception {
        Socket socket1 = new Socket("127.0.0.1",8888);
        Socket socket2 = new Socket(InetAddress.getByName("127.0.0.1"),8888);
    }// 客户端

也许你需要构造后设置一些参数而不是直接连接,可以使用如下方式。

public static void main(String[] args) throws Exception {
        Socket socket = new Socket();
        SocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        socket.connect(address, 1000);
        a(socket);
    }// 客户端

    public static void a(Socket socket) {
        try {
            OutputStream out = socket.getOutputStream();
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out));
            bw.append("GET /test HTTP/1.1\r\n");
            bw.append("Host: 127.0.0.1:8888\r\n");
            bw.append("\r\n");

            bw.flush();
            socket.shutdownOutput();

            InputStream in = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String line = null;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
            socket.shutdownInput();
        } catch (Exception e) {
            System.out.println(e);
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

Socket地址

通常的我们将客户端或服务端的连接地址封装到SocketAddress类中,达到复用连接参数的目的。SocketAddress有唯一的一个子类InetSocketAddress。Socket类的getRemoteSocketAddress()可以获取服务端的Address。getLocalSocketAddress()可以获取客户端的Address。

public static void main(String[] args) throws Exception {
        Socket socket = new Socket();
        SocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        socket.connect(address);

        InetSocketAddress socketAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
        System.out.println(socketAddress.getHostName() + ":" + socketAddress.getPort());
        InetSocketAddress localSocketAddress = (InetSocketAddress) socket.getLocalSocketAddress();
        System.out.println(localSocketAddress.getHostName() + ":" + localSocketAddress.getPort());
        a(socket);
    }// 客户端

代理服务器

要使用某个特定的代理服务器可以使用以下写法。

public static void main(String[] args) throws Exception {
        SocketAddress proxyAddress = new InetSocketAddress("127.0.0.1",8888);//代理服务器
        Proxy proxy = new Proxy(Proxy.Type.SOCKS,proxyAddress);
        Socket socket = new Socket(proxy);
        
        SocketAddress address = new InetSocketAddress("127.0.0.1", 8888);//要连接的服务器
        socket.connect(address);
        
        a(socket);
    }// 客户端

获取Socket的信息

socket对象有一些属性可以通过获取方法来访问。一旦Socket连接后这些属性将不可改变。远程端口通常是一个已知服务端的端口。而本地端口则是系统运行时从未使用的空闲端口中选择,通过这种方式多个不同的客户端就可以同时访问相同的服务。本地端口与本地主机的IP地址一同镶嵌入在出站IP包中,所以服务器可以向客户端上正确的端口返回数据。

getInetAddress();获取远程主机。getPort();获取远程端口。

getLocalAddress()获取本地主机。getLocalPort();获取本地端口。

关闭还是连接

要判断一个socket是否已经打开关键的两个方法是isConnected();它指示socket是否连接过。isClosed();它表示连接是否关闭。

public static void main(String[] args) throws Exception {
        Socket socket = new Socket();
        SocketAddress address = new InetSocketAddress("127.0.0.1", 8888);//要连接的服务器
        System.out.println(socket.isConnected());//true表示已经连接过
        System.out.println(socket.isClosed());//true表示未关闭
        System.out.println(socket.isBound());//true表示已绑定本地端口
        socket.connect(address);
        System.out.println(socket.isConnected());//true表示已经连接过
        System.out.println(socket.isClosed());//true表示未关闭
        System.out.println(socket.isBound());//true表示已绑定本地端口
        a(socket);
        System.out.println(socket.isConnected());//true表示已经连接过
        System.out.println(socket.isClosed());//true表示未关闭
        System.out.println(socket.isBound());//true表示已绑定本地端口
    }// 客户端

Socket选项

TCP_NODELAY:设置TCP_NODELAY为true可以确保包会尽可能快的发送,而无论包的大小。正常请求下,小数据包(一字节)在发送前会组合为更大的包。在发送另一个包之前,本地主机要等待远程系统对前一个包的确认。这成为Nagle算法。Nagle算法的问题是,如果远程系统没有足够快地将确认发回本地系统,那么依赖于小数据量信息稳定传输地应用程序会变得很慢。对于GUI程序这个问题尤其严重。在一个相当慢地网络中,即使简单地打字也会由于持续地缓冲而变得太慢。设置TCP_NODELAY为true可以打破这种缓冲模式,这样所有包一旦就绪就会立刻发送。

 

SO_LINGER:SO_LINGER指定了Socket关闭时如何处理未发送地数据报文。默认情况下close()方法将立刻返回,但系统仍然会尝试发送剩余地数据。如果延迟时间设为0,那么当Socket关闭时,所有未发送地数据包都将被丢弃。如果SO_LINGER打开而且延迟实际设置为任意正数,close()方法会阻塞,阻塞实际为指定的秒数,等待发送数据和接收确认。当过去相当秒数后,Socket关闭,所有剩余的数据都不会发送,也不会收到确认。如果底层Socket实现不支持SO_LINGER选项,这两个方法都会抛出SocketException异常。如果试图将延迟时间设置为一个负数,会抛出一个IllegalArgumentException异常。如果查找该属性返回-1则表示这个选项未使用,最大的延迟时间为65535秒,有些平台可能要更短一些。

 

SO_TIMEOUT:正常情况下,尝试从Socket读取数据时,read()调用会阻塞尽可能长的时间来得到足够的子节。设置SO_TIMEOUT可以确保这次调用阻塞的时间不会超过某个固定的毫秒数。当这个时间到期时就会抛出一个InterruptedIOException异常,你应当准备好捕获这个异常。不过Socket仍然是连接的。虽然这个read()调用失败了,但可以尝试读取该Socket。下一次调用可能会成功。超时时间按毫秒数给出。0为无限超时,这是默认值。当底层Socket实现不支持SO_TIMEOUT选项时,这两个方法都抛出SocketException异常。如果指定为负数,则抛出IllegalArgumentException异常。

SO_RCVBUF和SO_SNDBUF:SO_RCVBUF用于控制网络输入的缓冲区大小,SO_SNDBUF用于控制网络输出的缓冲区大小。虽然分为输入和输出两个设置,但实际上缓冲区通常会设置两者之间较小的一个。如果你有一个25Mb/s的Internet连接,但是数据传输的速率仅为1.5Mb/s那么可以尝试增加缓冲区大小,相反如果存在丢包和拥塞现象,则要减少缓冲区的大小。

 

SO_KEEPALIVE:如果打开了SO_KEEPALIVE客户端偶尔会通过一个空闲连接发送一个数据包,两小时一次。以确保服务器未崩溃。如果服务器没能相应这个包,客户端会持续尝试11分钟的时间直到接收到响应为止。如果12分钟内未接收到响应,客户端就会关闭socket。如果没有SO_KEEPALIVE,不活动的客户端可能会永久存在下去,而不会注意到服务器已经崩溃。

 

SO_REUSEADDR:一个Socket关闭时,可能不会立即释放本地端口,尤其是当Socket关闭时若仍有一个打开的连接,就不会释放本地端口。有时会等待一段时间,确保接收到所有要发送到这个端口的延迟数据包.这样造成的问题时,当前Socket已经关闭但端口在较短时间内不能被别的应用使用。开启SO_REUSEADDR允许另一个Socket绑定到这个端口,即使此时仍有可能存在前一个Socket未接收的数据。要设置SO_REUSEADDR需要在绑定端口之前使用,这意味着必须要使用无参构造的Socket然后使用connect()方法打开连接。

public static void main(String[] args) throws Exception {
        Socket socket = new Socket("127.0.0.1", 8888);
        
        //TCP_NODELAY
        socket.setTcpNoDelay(true);
        socket.getTcpNoDelay();
        
        //SO_LINGER
        socket.setSoLinger(true, 10);
        socket.getSoLinger();
        
        //SO_TIMEOUT
        socket.setSoTimeout(0);
        socket.getSoTimeout();
        
        //SO_KEEPALIVE
        socket.setKeepAlive(true);
        socket.getKeepAlive();
        
        //SO_REUSEADDR
        socket.setReuseAddress(true);
        socket.getReuseAddress();
        
        //SO_RCVBUF
        socket.setReceiveBufferSize(100);
        socket.getReceiveBufferSize();
        
        //SO_SNDBUF
        socket.setSendBufferSize(100);
        socket.getSendBufferSize();

        a(socket);
    }// 客户端

Socket异常

如果试图在一个正在使用的端口上构造Socket或ServerSocket对象或者你没有权限使用这个端口则会抛出BindException异常。如果连接被远程主机拒绝,而拒绝的原因通常是由于主机忙或者没有进程在监听这个端口则会抛出ConnectException异常。如果连接已经超时则抛出NoRouteToHostException异常。当从网络接收的数据违反TCP/IP规范时,会抛出ProtocolException异常。

 

上一篇:socket指定SRCIP和SO_BINDTODEVICE的区别


下一篇:【网络编程实践】1.2 网络编程注意事项