博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java Socket 之 TCP Socket
阅读量:6370 次
发布时间:2019-06-23

本文共 8935 字,大约阅读时间需要 29 分钟。

InetAddress

InetAddress 类代表 IP 地址(Internet Protocol (IP) address)。 一个 InetAddress 对象由 IP 地址和主机名(host name)组成。

InetAddress 的子类 Inet4AddressInet6Address 分别代表了 IPv4IPv6 的 IP 地址。

通常我们在浏览器中通过输入主机名来访问一个网站,例如通过 developer.android.com 主机名访问 Android 官网。 而与网站建立连接使用的是 IP 地址,这就需要使用DNS(Domain Name Service) 来把主机名解析为 IP 地址,例如 developer.android.com 有多个IP地址,其中一个为 74.125.23.100

创建 InetAddress 实例

InetAddress 提供了许多静态的方法来创建 InetAddress 实例,最常用的是通过主机名来创建实例,当然也可以通过IP地址创建

public static InetAddress getByName(String host){}public static InetAddress[] getAllByName(String host){}public static InetAddress getByAddress(String host, byte[] addr){}public static InetAddress getByAddress(byte[] addr){}复制代码

参数 host 代表主机名,例如 developer.android.com,而参数 addr 就代表 IP 地址,只不过这个 IP 地址是用字节数组形式表示的。

InetAddress 还提供了获取本地主机的 InetAddress 实例的方法

public static InetAddress getLocalHost() {}复制代码

获取主机名和IP地址

既然 InetAddress 是由 IP 地址和主机名组成,那么就可以获取这些属性的值

public String getHostName() {}public String getHostAddress() {}public byte[] getAddress() {}public String toString() {    String hostName = holder().getHostName();    return ((hostName != null) ? hostName : "")        + "/" + getHostAddress();}复制代码

第二个方法和第三个方法都是获取主机的 IP 地址,只是表示形式不同而已。

举例

InetAddress[] addresses = InetAddress.getAllByName("developer.android.com");        for (InetAddress inetAddress : addresses) {            System.out.println("Host name: " + inetAddress.getHostName());            System.out.println("Host address: " + inetAddress.getHostAddress());            System.out.println("IP address: " + Arrays.toString(inetAddress.getAddress()));            System.out.println("-----");        }复制代码

一个主机可能有多个网络接口,例如以太网,WIFI, 而每一个接口可能有多个IP地址,最常见的就是 IPv4IPv6 的地址,因此通过主机名可以获取 InetAddress[]。这个例子的部分结果如下

Host name: developer.android.comHost address: 74.125.204.102IP address: [74, 125, -52, 102]-----Host name: developer.android.comHost address: 74.125.204.113IP address: [74, 125, -52, 113]复制代码

TCP 套接字

TCP 协议

IP 协议只是一个尽力而为(best-effort)的协议,它尝试分发每一个分组报文,但是在网络传输过程中,报文可能会丢失,顺序可能被打乱或者重复发送报文的情况。 而 TCP 协议构建于 IP 协议之上,它是一种面向连接,端到端的协议,提供了可靠的字符流通道,因为它在通信之前需要建立一个TCP连接,也就是握手消息(handshake message)交换。

Java 为 TCP 协议提供两个类:Socket 类和 ServerSocket 类。

SocketAddress

在学习 Socket 之前,先来看看 SocketAddress 类,这个类代表一个套接字地址 (socket address),一个套接字地址是由 IP 地址和端口号组成。 SocketAddress 是一个抽象类,而它的唯一子类就是 InetSocketAddress 类。 InetSocketAddress 其实是在 InetAddress 的基础上加了一个端口号。由于 InetSocketAddressInetAddress 很相似,因此不多做介绍了。

Socket

Socket 是两台机器进行通信的终端,它由 IP 地址和端口号定义。而 Socket 类实现的是客户端的套接字。

客户端在与服务器在通信之前需要建立 TCP 连接,这就需要提供本地 IP 地址和端口号以及服务器 IP 地址和端口号,这就是创建 Socket 对象需要提供的参数。

IP 地址识别主机,端口号识别主机上的应用程序。

public Socket(String remoteHost, int remotePort) {}public Socket(InetAddress remoteAddr, int remotePort) {}public Socket(String remoteHost, int remotePort, InetAddress localAddr,int localPort) {}public Socket(InetAddress remoteAddr, int remotePort, InetAddress localAddr,int localPort) {}复制代码

在构造函数中如果不指定 IP 地址和端口号,就会选择一个本地的可靠的 IP 地址和端口号。 通常我们都会选择前二个构造函数来创建 Socket 实例。

当然我们也可以调用无参的构造函数来创建 Socket 实例

public Socket() {}复制代码

用无参的构造函数创建 Socket 实例后,需要调用 connect() 方法来建立连接

public void connect(SocketAddress endpoint) throws IOException {}public void connect(SocketAddress endpoint, int timeout) throws IOException {}复制代码

参数 SocketAddress endpoint 代表了服务器的 IP 地址和端口号,同时我们需要注意到,参数 int timeout 定义了超时的连接时间,这个是有参数构造函数无法提供的功能。

其实无论是有参还是无参的构造函数,最终都要调用 connect() 方法来建立连接。 只是有参数的构造函数设置的超时时间是 0,也就是不会超时,建立连接时会阻塞线程,直到连接建立或者错误发生。而无参的构造函数,会让开发者手动调用 connect() 方法并设置超时时间来建立连接,避免长时间阻塞的情况。

当连接建立后,也就是成功获取了 Socket 实例,就可以通过这个实例获取本地和服务器的 IP 地址和端口号

// 获取服务器IP地址和端口号public InetAddress getInetAddress() {}public int getPort() {}// 获取连接的本地IP地址和端口号public InetAddress getLocalAddress() {}public int getPort() {}// 获取服务器IP地址和端口号public SocketAddress getRemoteSocketAddress() {}public SocketAddress getLocalSocketAddress() {}复制代码

ServerSocket

前面说过 Socket 类是实现客户端套接字,而 ServerSocket 类实现的是服务器端套接字。

public ServerSocket(int port) {}public ServerSocket(int port, int queueLimit) {}public ServerSocket(int port, int queueLimit, InetAddress localAddr) {}复制代码

创建 ServerSocket 实例需要绑定一个端口号,方便用 accept() 方法来监听这个端口号上的所有客户端请求。

参数 int queueLimit 定义请求队列的长度,如果超过这个长度,会拒绝这个连接请求。

如果我们有特殊的本地 IP 地址需要,我们可以指定本地的某一个 IP 地址,也就是第三个参数。

而如果我们调用了无参的构造函数

public ServerSocket() {}复制代码

需要调用 bind() 方法绑定端口号

public void bind(SocketAddress endpoint){}public void bind(SocketAddress endpoint, int queueLimit){}复制代码

其实效果与有参数的构造函数没有什么区别。

当创建 ServerSocket 实例后,我们需要用 accept() 方法来监听在这个端口上的请求

public Socket accept() throws IOException {}复制代码

一旦接受了这个请求,就会返回一个 Socket 实例,我们就可以用这个实例与客户端进行通信。需要注意的是,这个监听会一直阻塞当前线程,直到有请求发生并建立连接,或者发生错误。

创建 TCP 客户端和服务器

现在来创建一个基于 TCP 套接字的客户端和服务器。

首先创建一个客户端,它会先发送信息到服务器,然后读取服务器返回的信息。

public class TCPClient1 {    public static void main(String[] args) {        String message = "hello";        Socket socket = null;        try {            // 1. create a socket bound to port 8890 and connected to server            socket = new Socket(InetAddress.getLocalHost(), 8890);            System.out.println("Connected to server...");            // 2. send message to server            OutputStream out = socket.getOutputStream();            out.write(message.getBytes());            // 3. close socket output            socket.shutdownOutput();            // 4. read message from server            InputStream in = socket.getInputStream();            BufferedReader br = new BufferedReader(new InputStreamReader(in));            String line;            System.out.println("Server said:");            while ((line = br.readLine()) != null) {                System.out.println(line);            }        } catch (IOException e) {            e.printStackTrace();        } finally {            // 5. close socket            try {                if (socket != null) {                    socket.close();                }            } catch (IOException e) {                e.printStackTrace();            }        }    }}复制代码

第一步,创建 Socket 实例,它会与服务器建立连接。

第二步,从创建的 Socket 实例获取输出流,并向服务器发送数据。

第三步,关闭 Socket 的输出流。这一步很关键,因此服务器读取客户端的数据时候,需要知道客户端数据什么时候发送完毕了,我们通过关闭输出流来通知服务器数据发送完毕。

第四步,获取输入流,然后读取服务器返回的数据。 这里是通过一个 while 循环来读取服务器数据,如果服务器没有数据了,readLine() 方法会一直阻塞,如果服务器关闭输出流,readLine() 就会返回 null

第五步,关闭 Socket,同时也关闭了与 Socket 相关的输入输出流。

客户端创建完了,现在来创建服务器端。服务器端先获取客户端信息,然后返回一个响应给客户端。

public class TCPServer1 {    public static void main(String[] args) {        ServerSocket serverSocket = null;        Socket socket = null;        try {            // 1. create server socket bound to port 8890            serverSocket = new ServerSocket(8890);            // 2. listen for a request connection, blocks until a connection is made            socket = serverSocket.accept();            System.out.println("Handling client: " + socket.getRemoteSocketAddress());            // 3. get message from client            InputStream in = socket.getInputStream();            BufferedReader br = new BufferedReader(new InputStreamReader(in));            String line;            System.out.println("Client said:");            while ((line = br.readLine()) != null) {                System.out.println(line);            }            // 4. write message to client            OutputStream out = socket.getOutputStream();            out.write("Welcome!".getBytes());        } catch (IOException e) {            e.printStackTrace();        } finally {            try {                // 5. close socket and server socket                if (socket != null) {                    socket.close();                }                if (serverSocket != null) {                    serverSocket.close();                }            } catch (IOException e) {                e.printStackTrace();            }        }    }}复制代码

第一步,创建一个 ServerSocket 实例,并且绑定相应的端口。

第二步,监听绑定的端口上请求连接,这个监听会阻塞当前线程,直到连接建立或发生错误。

第三步,获取输入流,然后循环读取信息。 如果客户端数据发送完毕,readLine() 方法会阻塞,直到客户端关闭了输出流或者连接发生错误才会返回 null。 这也是为什么在客户端的代码中,发送完信息后要关闭输出流的原因。

第四步,获取输出流,向客户端输出信息。

第五步,关闭创建的 SocketServerSocket

当先运行服务器再启动客户端后,服务器会输出

Handling client: /173.10.2.51:52793Client said:hello复制代码

而客户端会说

Connected to server...Server said:Welcome!复制代码

改进 TCP 服务器端

上面的服务器端设计有明显的缺陷,那就是它处理了只能处理一个客户端请求,怎么改进呢? 可能通过加一个无限循环,不断的监听指定端口上的请求连接,然后处理

serverSocket = new ServerSocket(8890);            // NOTE: 无限循环监听客户端请求连接并处理请求            while (true) {                socket = serverSocket.accept();                System.out.println("Handling client: " + socket.getRemoteSocketAddress());                InputStream in = socket.getInputStream();                BufferedReader br = new BufferedReader(new InputStreamReader(in));                String line;                System.out.println("Client said:");                while ((line = br.readLine()) != null) {                    System.out.println(line);                }                OutputStream out = socket.getOutputStream();                out.write("Welcome!".getBytes());            }复制代码

通过 while(true) 的无限循环,可以在处理完一个客户端后,再继续监听下一个客户端的请求连接,再处理,然后接着循环,这样就能拥有了处理多个客户端的能力。

改进后的服务器端拥有了处理多个客户端请求的能力,但是它却是在单线程处理的,效率不高,因此需要为服务器端加入多线程处理客户端请求的能力,我们可以使用线程池,这个情况后面再谈。

转载地址:http://tcyqa.baihongyu.com/

你可能感兴趣的文章
物联网影响商业发展三要素
查看>>
China Unicom and Chunghwa Telecom work together&nb
查看>>
Java图片上查找图片算法
查看>>
Python fabric实现远程操作和部署
查看>>
详解Java中staitc关键字
查看>>
前中情局局长:FBI目的是从根本上改善iPhone
查看>>
大隐隐于市,你身边的那些安全隐患你都知道么?
查看>>
物联网市场迅猛发展 “中国芯”如何把握机会?
查看>>
aws 上使用elb 的多域名问题
查看>>
环球花木网的目标就是致力于打造成为“园林相关行业的专业性门户网站
查看>>
《编写高质量代码:改善c程序代码的125个建议》—— 建议14-1:尽量避免对未知的有符号数执行位操作...
查看>>
《C语言编程魔法书:基于C11标准》——2.2 整数在计算机中的表示
查看>>
全球程序员编程水平排行榜TOP50,中国排名第一
查看>>
HDFS 进化,Hadoop 即将拥抱对象存储?
查看>>
Edge 浏览器奇葩 bug:“123456”打印成“114447”
查看>>
Sirius —— 开源版的 Siri ,由 Google 支持
查看>>
《OpenGL ES应用开发实践指南:Android卷》—— 2.7 小结
查看>>
《Windows Server 2012活动目录管理实践》——第 2 章 部署第一台域控制器2.1 案例任务...
查看>>
Java Date Time 教程-时间测量
查看>>
Selector.wakeup实现注记
查看>>