2529 2018-04-14 2020-06-25

前言:Java是一门活跃在服务后端的语言,而Java的网络编程提供客户端/服务端连接通信的底层实现。

一、IP地址

1、InetAddress

在互联网上通信,IP地址是必不可少的。Java提供一个与IP地址相关的类,如下

public class InetAddress implements Serializable {
    static final int IPv4 = 1;
    static final int IPv6 = 2;

    // 默认构造方法,即只能在本包内被访问到
    InetAddress() {
        holder = new InetAddressHolder();
    }

    // 值得注意的是里面一些静态方法
    public static InetAddress getByName(String host)
        throws UnknownHostException {
        return InetAddress.getAllByName(host)[0];
    }

    public static InetAddress[] getAllByName(String host)
        throws UnknownHostException {
        return getAllByName(host, null);
    }

    private static InetAddress[] getAllByName(String host, InetAddress reqAddr)
        throws UnknownHostException {

        if (host == null || host.length() == 0) {
            InetAddress[] ret = new InetAddress[1];
            ret[0] = impl.loopbackAddress();
            return ret;
        }

        boolean ipv6Expected = false;
        if (host.charAt(0) == '[') {
            // This is supposed to be an IPv6 literal
            if (host.length() > 2 && host.charAt(host.length()-1) == ']') {
                host = host.substring(1, host.length() -1);
                ipv6Expected = true;
            } else {
                // This was supposed to be a IPv6 address, but it's not!
                throw new UnknownHostException(host + ": invalid IPv6 address");
            }
        }

        // if host is an IP address, we won't do further lookup
        if (Character.digit(host.charAt(0), 16) != -1
            || (host.charAt(0) == ':')) {
            byte[] addr = null;
            int numericZone = -1;
            String ifname = null;
            // see if it is IPv4 address
            addr = IPAddressUtil.textToNumericFormatV4(host);
            if (addr == null) {
                // This is supposed to be an IPv6 literal
                // Check if a numeric or string zone id is present
                int pos;
                if ((pos=host.indexOf ("%")) != -1) {
                    numericZone = checkNumericZone (host);
                    if (numericZone == -1) { /* remainder of string must be an ifname */
                        ifname = host.substring (pos+1);
                    }
                }
                if ((addr = IPAddressUtil.textToNumericFormatV6(host)) == null && host.contains(":")) {
                    throw new UnknownHostException(host + ": invalid IPv6 address");
                }
            } else if (ipv6Expected) {
                // Means an IPv4 litteral between brackets!
                throw new UnknownHostException("["+host+"]");
            }
            InetAddress[] ret = new InetAddress[1];
            if(addr != null) {
                if (addr.length == Inet4Address.INADDRSZ) {
                    ret[0] = new Inet4Address(null, addr);
                } else {
                    if (ifname != null) {
                        ret[0] = new Inet6Address(null, addr, ifname);
                    } else {
                        ret[0] = new Inet6Address(null, addr, numericZone);
                    }
                }
                return ret;
            }
        } else if (ipv6Expected) {
            // We were expecting an IPv6 Litteral, but got something else
            throw new UnknownHostException("["+host+"]");
        }
        return getAllByName0(host, reqAddr, true);
    }

    public static InetAddress getLocalHost() throws UnknownHostException {

        SecurityManager security = System.getSecurityManager();
        try {
            String local = impl.getLocalHostName();

            if (security != null) {
                security.checkConnect(local, -1);
            }

            if (local.equals("localhost")) {
                return impl.loopbackAddress();
            }

            InetAddress ret = null;
            synchronized (cacheLock) {
                long now = System.currentTimeMillis();
                if (cachedLocalHost != null) {
                    if ((now - cacheTime) < maxCacheTime) // Less than 5s old?
                        ret = cachedLocalHost;
                    else
                        cachedLocalHost = null;
                }

                // we are calling getAddressesFromNameService directly
                // to avoid getting localHost from cache
                if (ret == null) {
                    InetAddress[] localAddrs;
                    try {
                        localAddrs =
                            InetAddress.getAddressesFromNameService(local, null);
                    } catch (UnknownHostException uhe) {
                        // Rethrow with a more informative error message.
                        UnknownHostException uhe2 =
                            new UnknownHostException(local + ": " +
                                                     uhe.getMessage());
                        uhe2.initCause(uhe);
                        throw uhe2;
                    }
                    cachedLocalHost = localAddrs[0];
                    cacheTime = now;
                    ret = localAddrs[0];
                }
            }
            return ret;
        } catch (java.lang.SecurityException e) {
            return impl.loopbackAddress();
        }
    }   
}

2、InetAddress的使用

我们来看下InetAddress的一些常用方法。

InetAddress inetAddress = InetAddress.getLocalHost();
System.out.println(inetAddress.getHostAddress());
System.out.println(inetAddress.getHostName());

InetAddress inetAddress1 = InetAddress.getByName("www.baidu.com");
System.out.println(inetAddress1.getHostAddress());
System.out.println(inetAddress1.getHostName());

InetAddress[] inetAddress2 = InetAddress.getAllByName("www.baidu.com");
for (InetAddress temp : inetAddress2) {
    System.out.println(temp.getHostAddress());
    System.out.println(temp.getHostName());
}
// 输出结果如下
192.168.187.1
HK
183.232.231.172
www.baidu.com
183.232.231.172
www.baidu.com
183.232.231.173
www.baidu.com

二、TCP通信

TCP协议是面向连接的通信协议,在两台计算机之间提供可靠无差错的数据传输。每次连接的创建都需要经历“三次握手”。

Java提供了两个类用于实现TCP通信,一个是用于服务器端的ServerSocket类,另一个是用于客户端的Socket类。

1、ServerSocket

public class ServerSocket implements java.io.Closeable {
    /**
     * Various states of this socket.
     */
    private boolean created = false;
    private boolean bound = false;
    private boolean closed = false;
    private Object closeLock = new Object();

    /**
     * The implementation of this Socket.
     */
    private SocketImpl impl;

    /**
     * Are we using an older SocketImpl?
     */
    private boolean oldImpl = false;

    // 包内访问
    ServerSocket(SocketImpl impl) {
        this.impl = impl;
        impl.setServerSocket(this);
    }


    public ServerSocket() throws IOException {
        setImpl();
    }

    // 常用
    public ServerSocket(int port) throws IOException {
        this(port, 50, null);
    }

    public ServerSocket(int port, int backlog) throws IOException {
        this(port, backlog, null);
    }

    public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
        setImpl();
        if (port < 0 || port > 0xFFFF)
            throw new IllegalArgumentException(
                       "Port value out of range: " + port);
        if (backlog < 1)
          backlog = 50;
        try {
            bind(new InetSocketAddress(bindAddr, port), backlog);
        } catch(SecurityException e) {
            close();
            throw e;
        } catch(IOException e) {
            close();
            throw e;
        }
    }

    private void setImpl() {
        if (factory != null) {
            impl = factory.createSocketImpl();
            checkOldImpl();
        } else {
            // No need to do a checkOldImpl() here, we know it's an up to date
            // SocketImpl!
            impl = new SocksSocketImpl();
        }
        if (impl != null)
            impl.setServerSocket(this);
    }

    public InetAddress getInetAddress() {
        if (!isBound())
            return null;
        try {
            InetAddress in = getImpl().getInetAddress();
            SecurityManager sm = System.getSecurityManager();
            if (sm != null)
                sm.checkConnect(in.getHostAddress(), -1);
            return in;
        } catch (SecurityException e) {
            return InetAddress.getLoopbackAddress();
        } catch (SocketException e) {
            // nothing
            // If we're bound, the impl has been created
            // so we shouldn't get here
        }
        return null;
    }

    public int getLocalPort() {
        if (!isBound())
            return -1;
        try {
            return getImpl().getLocalPort();
        } catch (SocketException e) {
            // nothing
            // If we're bound, the impl has been created
            // so we shouldn't get here
        }
        return -1;
    }

    public Socket accept() throws IOException {
        if (isClosed())
            throw new SocketException("Socket is closed");
        if (!isBound())
            throw new SocketException("Socket is not bound yet");
        Socket s = new Socket((SocketImpl) null);
        implAccept(s);
        return s;
    }

    public void close() throws IOException {
        synchronized(closeLock) {
            if (isClosed())
                return;
            if (created)
                impl.close();
            closed = true;
        }
    }
}

Socket源代码形式大致同ServerSocket,读者可自行查看。

2、模拟TCP对话

代码如下

public class Server {

    public static void main(String[] args) throws UnknownHostException, IOException {
        ServerSocket serverSocket = new ServerSocket(2222);
        serverSocket.getInetAddress();
        System.out.println("服务端等待连接....");
        Socket client = serverSocket.accept();
        System.out.println("与客户端 " + client.getInetAddress().toString() + " 建立连接");

        BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
        PrintWriter pw = new PrintWriter(client.getOutputStream());
        pw.println("欢迎连接到服务器^_^");
        pw.flush();
        Scanner scanner;
        String resp;
        String clientMessage = br.readLine();
        while (!clientMessage.equals("bye")) {
            System.out.println("客户端说:" + clientMessage);
            scanner = new Scanner(System.in);
            System.out.print("服务端说:");
            resp = scanner.nextLine();
            pw.println(resp);
            pw.flush();
            if (resp.equals("bye")) {
                break;
            }
            clientMessage = br.readLine();
        }
        br.close();
        pw.close();
        System.out.println("服务端:连接结束");
    }
}

public class Client {

    public static void main(String[] args) throws UnknownHostException, IOException {
        Socket socket = new Socket(InetAddress.getLocalHost(), 2222);

        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintWriter pw = new PrintWriter(socket.getOutputStream());

        Scanner scanner;
        String req;
        String serverMessage = br.readLine();
        while (!serverMessage.equals("bye")) {
            System.out.println("服务端说:" + serverMessage);
            scanner = new Scanner(System.in);
            System.out.print("客户端说:");
            req = scanner.nextLine();
            pw.println(req);
            pw.flush();
            if (req.equals("bye")) {
                break;
            }
            serverMessage = br.readLine();
        }
        br.close();
        pw.close();
        System.out.println("客户端:连接结束");
    }
}

// 服务端输入输出如下
服务端等待连接....
与客户端 /192.168.187.1 建立连接
客户端说:hello
服务端说:hello too
客户端说:我叫一般般帅气
服务端说:我叫一般般可爱
客户端说:奥
服务端说:哦
服务端:连接结束

// 客户端输入输出如下
服务端说:欢迎连接到服务器^_^
客户端说:hello
服务端说:hello too
客户端说:我叫一般般帅气
服务端说:我叫一般般可爱
客户端说:奥
服务端说:哦
客户端说:bye
客户端:连接结束

三、UDP连接

UDP是一种面向无连接的协议(面向报文的),与TCP不同,它在通信时客户端和服务器端不用建立连接,因此不能保证通信的安全性。Java提供了两个类用于支持UDP,其中DatagramPacket用于封装要发送的数据包,而DatagramSocket类用于发送或接受数据包(DatagramPacket)的套接字。

1、DatagramPacket

在发送或接受数据时,必须先创建DatagramPacket对象用于封装数据,然后通过相应的方法可以得到发送或接受到的数据包的信息。在发送数据包中需绑定接收端的IP地址和端口号,系统自动添加发送端的IP地址和端口号。下面是常用的一些方法

方法声明功能描述
DatagramPacket(byte[] buf, int length)构造DatagramPacket,用来接收长度为length的数据包buf
DatagramPacket(byte[] buf, int length, InetAddress address, int port)构造数据包,用来将长度为length的数据包发送到指定主机上的指定端口
DatagramPacket(byte[] buf, int offset, int length)构造DatagramPacket,用来接收长度为length的数据包,在缓存区指定了偏移量
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)构造数据包,用来将长度为length、偏移量为offset的数据包发送到指定主机上的指定端口
InetAddress getAddress()返回某台机器的IP地址(他方IP地址),此数据包要发往该机器或者从该机器接受到的
int getPort()返回某台主机的端口号
void setData(byte[] buf, int offset, int length)为此包设置数据缓冲区。此方法设置数据包的数据、长度和偏移量
byte[] getData()返回数据缓冲区
int getLength()返回将要发送或接受到的数据长度

2、DatagramSocket

DatagramSocket类表示用来发送和接受数据包的套接字,创建DatagramSocket类对象,就可以发送或接受DatagramPacket数据包。

数据包套接字是包投递的发送和接受点,每个在数据包套接字上发送或接受的数据包都是单独编址和路由的。从一台机器发送到另一台机器的多个数据包可能选择不同的路由,也可能按不同的顺序到达。

由于发送和接受的方式不同,所以在创建发送端和接收端DatagramSocket对象时,使用的构造方法也有所不同。如下

方法声明功能描述
DatagramSocket()构造数据包套接字并将其绑定到本地主机上任何可用的端口
DatagramSocket(int port)创建数据包套接字并将其绑定到本地主机上指定端口
DatagramSocket(int port, InetAddress address)创建数据包套接字,将其绑定到指定的本地地址。本方法一般用于多网卡多IP的主机
void send(DatagramPacket packet)从此套接字发送数据包。数据包包含的信息为:将要发送的数据、其长度、远程主机的IP地址及端口号
void receive(DatagramPacket packet)当此方法返回时,DatagramPacket的缓冲区填充了接受的数据。数据包也包含发送方的IP地址及端口号,此方法在接受到数据包前一直阻塞。数据包对象的length字段包含所接受信息的长度,如果接受信息比包的长度长,该信息将被截短
void close()关闭此数据包套接字

3、模拟UDP

public class UServer extends Thread {

    byte[] buf;

    private DatagramSocket socket;

    private DatagramPacket packet;

    public UServer() throws Exception {
        socket = new DatagramSocket(2222);
        System.out.println("服务器在端口" + socket.getLocalPort() + "进行监听发送过来的数据包....");
    }

    @Override
    public void run() {
        while (true) {
            try {
                buf = new byte[1024];
                packet = new DatagramPacket(buf, buf.length);
                socket.receive(packet);// 会阻塞,直到接受到数据
                InetAddress hostAddress = packet.getAddress();
                int port = packet.getPort();
                String data = new String(packet.getData());

                System.out.println("收到" + hostAddress.getHostAddress() + " 端口号:" + port + ",发送的信息:" + data);
                String sendData = InetAddress.getLocalHost().getHostName() + " 已收到你发送的消息";
                buf = sendData.getBytes();
                packet = new DatagramPacket(buf, buf.length, hostAddress, port);
                socket.send(packet);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        new UServer().start();
    }
}

public class UClient {

    public static void main(String[] args) throws Exception {
        byte[] buf = "这是测试UDP的信息".getBytes();
        DatagramPacket packet = new DatagramPacket(buf, buf.length, InetAddress.getLocalHost(), 2222);
        DatagramSocket socket = new DatagramSocket(2221);
        socket.send(packet);

        buf = new byte[1024];
        packet = new DatagramPacket(buf, buf.length);
        socket.receive(packet);
        String receiveData = new String(packet.getData());
        System.out.println("从服务器" + packet.getAddress().getHostName() + "发送的内容为:\n" + receiveData);
        socket.close();
    }
}
// 输入输出结果如下
服务器在端口2222进行监听发送过来的数据包....
收到192.168.187.1 端口号:2221,发送的信息:这是测试UDP的信息

从服务器HK发送的内容为:
HK 已收到你发送的消息

更多关于网络编程的特性,需要在实践中慢慢摸索。

总访问次数: 242次, 一般般帅 创建于 2018-04-14, 最后更新于 2020-06-25

进大厂! 欢迎关注微信公众号,第一时间掌握最新动态!