Socket网络编程——(一)

什么是Socket

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。端点由IP地址和端口号共同组成,简单的说它是IP地址和端口结合的协议。

常用的套接字
流式套接字:流套接字用于提供面向连接、可靠的数据传输服务,该服务将保证数据能够实现无差错、无重复发送,并按顺序接收(基于TCP)。
数据报套接字:数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据(基于UDP)。

简单的介绍下TCP和UDP。TCP是面向连接的,保证可靠的数据传输,每次建立连接都需要经历三次握手,数据传输完成都需要经历4次挥手断开连接,由于TCP是面向连接的所以只能用于端到端的通信。而UDP是面向无连接的通讯协议,每次发送数据不需要建立连接,因此可以用于广播发送并不局限于端到端。

本篇主要学习ServerSocket和Socket的用法,它们分别对应了服务端的套接字和客户端的套接字,都是基于TCP协议封装的。

ServerSocket构造方法

ServerSocket() 创建一个未绑定到任何端口的服务器套接字
ServerSocket(int port) 创建一个绑定到指定端口的服务器套接字。
ServerSocket(int port, int backlog) 创建一个绑定到指定端口的服务器套接字,backlog是客户端连接请求的队列长度。
ServerSocket(int port, int backlog, InetAddress bindAddr) 创建一个绑定到指定端口、具有指定大小的请求队列、绑定到本地IP地址的服务器套接字。

简单的说明一下,第一个构造创建了一个未绑定任何端口的套接字,需要我们调用bind方法手动绑定。第2、3构造方法在创建的时候就指定了要绑定的端口。第三个构造方法在创建的时候就指定了要绑定的端口和本地IP,如果不指定IP会自动分配一个可用的本机IP。
需要特别理解backlog这个参数
backlog用来设置客户端连接请求队列的长度,客户端的连接请求是由操作系统负责管理的,操作系统会把连接请求放入到一个先进先出的队列里,通常操作系统会规定队列的最大长度为50。当队列到达上限50后再有其他请求到来就会被拒绝掉。只有当服务器执行serverSocket.accept(),把请求取出后,队列才能腾出空位加入新的请求。如果连接请求被拒绝,则会抛出ConnectionException。

ServerSocket相关API

bind(SocketAddress endpoint):绑定到特定的地址(IP地址和端口)。

accept() : 监听,当有连接请求到来就接收它返回一个新的Socket来和客户端通信,这个方法会阻塞直到连接请求到来

setSoTimeout(int timeout):设置连接超时,当设置了该选项,accept()方法在该时间段没有连接请求到来会抛出异常。

setReuseAddress(boolean on):
设置端口重用。当ServerSocket关闭时,如果网络上还有发送到该ServerSocket的数据,这个ServerSocket不会立即释放本地端口,而是等待一段时间,以确保接收到了网络上发送过来的延迟数据,然后再释放端口。为了确保一个进程关闭了ServerSocket 后,即使操作系统还没释放端口,同一个主机上的其他进程任然可以立刻重用该端口,可以调用ServerSocket .setResuseAddress(true)方法。需要注意两点:1、该方法必须在绑定到指定端口之前调用,否则无效。2、绑定到该公用端口的其他套接字也需要调用setReuseAddress方法。

setReceiveBufferSize (int size):
为所有accept方法返回的socket对象设置接收缓存区大小,单位为字节,默认大小与操作系统有关。一般说来,传输大的连续的数据块(基于HTTP或FTP协议的数据传输)可以使用较大的缓冲区,这可以减少传输数据的次数,从而提高传输数据的效率。而对于交互式的通信如Telent或者希望即时获取数据如游戏,则应该采用小的缓冲区,确保能及时把小批量的数据发送给对方。

setPerformancePreferences(int connectionTime,int latency,int bandwidth):
设置服务器套接字的性能首选项。用来设置短连接时间、低延迟和高带宽的相对重要性。

Socket构造方法

Socket() 创建一个未连接的套接字
Socket(Proxy proxy) 创建一个未连接的套接字,该套接字使用特定的代理 。
Socket(SocketImpl impl) 使用用户指定的socketimpl创建未连接的套接字。
Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号。
Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址处的指定端口号。
Socket(String host, int port, InetAddress localAddr, int localPort) 创建套接字并将其连接到指定远程主机上的指定端口。套接字还将绑定到指定的本地地址和端口。

构造方法还有其他,用法都类似,需要注意的是创建未连接的套接字需要我们自己手都调用socket.connect()方法进行连接,而如果调用那些创建时就进行连接的构造不需要再调用connect()方法进行连接。通常都是创建未连接的套接字,因为我们往往需要在连接之前对它进行一些初始化的配置。

Socket相关API

很多api的用法和ServerSocket的一致,只说几个特殊的。
setSoTimeout(int timeout):设置读超时,当此选项设置为非零时间,对此套接字关联的输入流的read()调用将仅阻塞此时间量,超时将抛出SocketTimeoutException: Read timed out。

setKeepAlive(boolean on)
当设置为true的时候,底层的TCP实现会监视该连接是否有效。当通信的两端套接字两个小时内没有数据交换(注意:实际值取决于实现),TCP实现会自动向远程套接字发送keepalive探测包,如果远程套接字响应了,说明连接正常。如果远程套接字没响应,TCP实现会继续探测,一定时间后依旧没响应代表远程服务器可能崩溃,TCP尝试关闭socket连接。默认值为 false, 表示TCP 不会监视连接是否有效, 不活动的客户端套接字可能会永远存在下去, 而不会注意到服务器已经关闭。可能的3种情况
1、远程套接字以预期的ACK响应。TCP将在另外2小时不活动后发送另一个探测器。
2、远程套接字以RST响应,该RST告诉本地TCP对等主机已崩溃并重新启动。套接字已关闭。
3、对等端没有响应。套接字已关闭。

setOOBInline(boolean on)
默认情况下,此选项被禁用,在套接字上接收的TCP紧急数据将被静默丢弃。如果用户希望接收紧急数据,则必须启用此选项。启用后,紧急数据与正常数据一起接收。紧急数据是通过sendUrgentData发出的一个单字节数据,它不经过发送缓冲区而是直接发送出去的,需要注意的是对于服务器端紧急数据和普通数据是混在一起的,服务器并不知道客户端发送到数据是通过sendUrgentData发出的,还是通过OutputStream发出的。

setSendBufferSize(int size)
设置发送缓冲区大小(size>0),最好不要将发送缓冲区设得太小,太小会导致传输数据过于频繁,从而降低网络传输的效率。

setTcpNoDelay(boolean on)
这个方法的作用是开启或关闭Nagle算法,默认Nagle算法是开启的,通过setTcpNoDelay(true)可以关闭该算法。它有两个作用:a、Nagle算法要求一个TCP连接上最多只能有一个未被确认的小分组,在该小分组的确认到来之前,不能发送其他小分组。也就是说发送方发送了一个分组后,只有等到接收方回复的ack控制段才发送下一个分组;2、如果启用该算法数据只有在写缓存中累积到一定量之后,才会被发送出去,这样通过减少数据传输次数来提高了网络利用率。如果关闭该算法,在前一个分组的确认没到来时依旧可以发送下一个分组并且客户端每发送一次数据,无论数据包的大小都会将这些数据发送出去。

setSoLinger(boolean on, int linger):
这个设置仅影响套接字的关闭。
默认为false,当执行socket的close()方法会立即返回,如果此时还有数据没发送完将由底层系统来接管输出流,尝试将缓冲区数据发送出去。
如果设置为setSoLinger(true, 0)调用socket.close()关闭套接字会立即返回,如果缓冲区还有数据未发送直接抛弃,并发送RST结束命令给对方。
如果设置为setSoLinger(true, n),关闭套接字时如果缓冲区还有数据没发送,最长阻塞n毫秒,在这个时间内如果缓冲区数据发送了close方法就返回,如果超过200毫秒任然没发送完,剩下的数据直接抛弃,立即返回。

通过一个简单的例子来认识下Socket:
编写了服务端Socket和客户端Socket,客户端连接服务器后向服务器发送数据,服务端收到后回送给客户端一个信息,当服务端收到客户端发送的bye也给客户端回送一个bye,然后客户端和服务端断开连接。这里的客户端和服务端都是在本机上。
服务端:

public class Server {
    public static void  main(String args[]) throws IOException {
        //创建服务端套接字,监听20000端口
        ServerSocket server=createSocket();
        initServerConfig(server);
        System.out.println("服务端准备就绪>> IP:"+server.getLocalSocketAddress().toString()+" PORT:"+server.getLocalPort());
        while (true){
            //监听
            Socket client=null;
            try {
                client=server.accept();
                System.out.println("有客户端连接:IP:"+client.getInetAddress().getHostAddress()+" PORT:"+client.getPort());
                //开个线程去处理和客户端的交互
                new ClientHandler(client).start();
            }catch (SocketTimeoutException e){

            }
        }
    }

    private static void initServerConfig(ServerSocket server) throws IOException {
        server.setSoTimeout(5000);
        server.setReuseAddress(true);
        server.setReceiveBufferSize(64*1024*1024);
        server.setPerformancePreferences(1,1,1);
        server.bind(new InetSocketAddress(20000));
    }

    private static ServerSocket createSocket() throws IOException {
        ServerSocket serverSocket=new ServerSocket();
        return serverSocket;
    }

    private static class ClientHandler extends Thread{

        private Socket mClient;
        private boolean flag=true;

        public ClientHandler(Socket socket){
            this.mClient=socket;
        }
        @Override
        public void run() {
            super.run();
            BufferedReader reader=null;
            PrintStream writer=null;
            try {
                //获取输入流 接收客户端的输入 mClient.getInputStream()
                reader=new BufferedReader(new InputStreamReader(mClient.getInputStream()));
                //获取输出流 用于向客户端发送数据 mClient.getOutputStream()
                writer=new PrintStream(mClient.getOutputStream());

                //读取客户端发送到数据
                do {
                    String msg=reader.readLine();
                    System.out.println("Client:"+msg);
                    if (msg.equalsIgnoreCase("bye")){
                        writer.println("bye");
                        flag=false;
                    }else {
                        writer.println("len="+msg.length());
                    }
                }while (flag);
            } catch (IOException e) {
                e.printStackTrace();
            }  finally {
                try {
                    //客户端退出后释放资源
                    reader.close();
                    writer.close();
                    mClient.close();
                    System.out.println("客户端退出");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客户端:

public class Client {
    public static void main(String args[]) throws IOException {
        Socket socket=createSocket();
        initSocketConfig(socket);
        //连接到本地 端口为20000的服务端 连接超时5000
        socket.connect(new InetSocketAddress(InetAddress.getLocalHost(),20000),5000);
        System.out.println("成功连接到服务器");

        //输入流 用于获取服务端发送到的数据 socket.getInputStream()
        BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
        //输出流 用于向服务端发送数据 socket.getOutputStream()
        PrintStream writer=new PrintStream(socket.getOutputStream());

        //键盘流
        BufferedReader cin=new BufferedReader(new InputStreamReader(System.in));
        boolean flag=true;
        do {
            String line=cin.readLine();
            writer.println(line);//发送数据
            String rcv=reader.readLine();//接收数据
            System.out.println("Server:"+rcv);
            if (rcv.equalsIgnoreCase("bye")){
                flag=false;
            }
        }while (flag);

        //释放资源
        System.out.println("客户端退出");
        cin.close();
        writer.close();
        reader.close();
        socket.close();
    }

    private static void initSocketConfig(Socket socket) throws SocketException {
        socket.setSoTimeout(5000);//读超时时间
        socket.setReuseAddress(true);
        socket.setKeepAlive(true);
        socket.setOOBInline(true);
        socket.setTcpNoDelay(true);
        socket.setSoLinger(true,200);
        socket.setSendBufferSize(64*1024*1024);
        socket.setReceiveBufferSize(64*1024*1024);
    }

    private static Socket createSocket() throws IOException {
        Socket socket=new Socket();
        return socket;
    }
}

结果:
客户端打印:
Socket网络编程——(一)_第1张图片
服务端打印:
Socket网络编程——(一)_第2张图片
可以看到服务端和客户端之间的通讯就是通过套接字进行的。

学习记录,如有错误欢迎指正。

你可能感兴趣的:(Socket学习)