目录
1、Java.net包
1. Socket通信相关类
2. URL和URI处理类
3. 网络地址和主机名解析类
4. 代理和认证相关类
5. 网络缓存和Cookie管理类
6. 其他网络相关工具类
2、什么是socket?
3、JDK中socket核心Api
4、核心源码
1、核心方法
2、本地方法
3、linux是怎么处理链接的?
5、demo验证
建立连接
发送数据
断开连接
1、Java.net包
java.net包提供了一组用于网络编程的类,支持开发网络应用程序和处理网络通信。java.net包可以大致分为六个部分:
1. Socket通信相关类
这些类用于实现基于 TCP 和 UDP 协议的网络通信。
- Socket:实现客户端的 TCP 套接字,允许程序与远程主机建立连接并传输数据。
- ServerSocket:实现服务器的 TCP 套接字,用于监听客户端连接并接受连接请求。
- DatagramSocket:用于 UDP 套接字通信,通过不可靠的无连接数据报协议传输数据。
- MulticastSocket:扩展了 DatagramSocket,用于支持多播组通信,允许数据发送到多个接收者。
- SocketAddress:表示一个套接字地址,通常用于绑定到特定 IP 地址和端口。
- InetSocketAddress:表示 IP 地址和端口的组合,继承自SocketAddress 。
2. URL和URI处理类
这些类用于处理统一资源定位符(URL)和统一资源标识符(URI)。
- URL:表示一个 URL(统一资源定位符),用于定位网络资源。支持从指定的 URL 中读取和解析数据。
- URI:表示一个 URI(统一资源标识符),提供对 URI 的结构化处理。
- URLStreamHandler:处理 URL 的协议细节,通常用于为不同协议(如 http、ftp)定义特定的处理逻辑。
- URLConnection:表示到 URL 所引用的资源的通信链接,可以处理资源的获取和发送。
- HttpURLConnection:继承自URLConnection ,用于处理 HTTP 协议的 URL 连接,支持 HTTP 请求和响应。
- JarURLConnection:继承自URLConnection ,用于从 JAR 文件中获取资源。
3. 网络地址和主机名解析类
这些类用于表示 IP 地址和解析主机名。
- InetAddress:表示 IP 地址,可以通过主机名或 IP 地址来查找和操作网络主机。
- Inet4Address:表示 IPv4 地址。
- Inet6Address:表示 IPv6 地址。
- NetworkInterface:表示网络接口(如以太网接口),用于获取和操作本地计算机的网络接口。
4. 代理和认证相关类
这些类用于处理网络代理和认证功能。
- Proxy:表示网络通信中的代理设置,支持 HTTP 和 SOCKS 等代理类型。
- ProxySelector:用于选择代理服务器的策略,可以根据 URI 的类型来选择合适的代理。
- Authenticator:用于实现网络请求中的认证机制,允许为 HTTP 和 FTP 请求提供用户名和密码等凭据。
- PasswordAuthentication:表示用户名和密码对,用于 Authenticator 类中的认证操作。
5. 网络缓存和Cookie管理类
这些类用于处理缓存和 Cookie 的存储和管理。
- CookieHandler:用于管理 HTTP 协议中的 Cookie。
- CookieManager:具体的 Cookie 管理器实现,允许存储和检索 Cookie。
- CacheRequest:表示缓存系统中的请求,允许将资源写入缓存。
- CacheResponse:表示缓存系统中的响应,允许从缓存中读取资源。
6. 其他网络相关工具类
一些提供额外网络功能的工具类。
- DatagramPacket:用于表示数据报,包含通过 DatagramSocket 发送或接收的数据。
- IDN:提供对国际化域名的处理,允许将 Unicode 域名转换为符合 Punycode 编码的 ASCII 字符串。
- NetworkInterface:表示计算机上的网络接口,允许列举和操作网络接口。
- StandardProtocolFamily:定义标准协议族(如 INET 和INET6),用于套接字通信。
2、什么是socket?
一个IP地址和一个端口号称为一个套接字(socket)。此术语出现在最早的TCP规范(RFC793, Page 5)中。我们知道进程通信的方法有管道、命名管道、信号、消息队列、共享内存、信号量,这些方法都要求通信的两个进程位于同一个主机。但是如果通信双方不在同一个主机又该如何进行通信呢?在计算机网络中有一个tcp/ip协议族,使用tcp/ip协议族就能达到我们想要的效果,如下图所示:
socket就是提供了tcp/ip协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用tcp/ip协议的功能了,使得我们可以在在不同的计算机或网络设备之间建立连接,允许数据的发送和接收。
3、JDK中socket核心Api
方法签名 | 作用 | |
构造函数 | public ServerSocket() | 创建未绑定的服务器套接字。稍后需要手动调用 bind() 来绑定到特定的端口。 |
public ServerSocket(int port) | 创建绑定到指定端口的服务器套接字,默认使用所有可用的网络接口(IP 地址)。 | |
public ServerSocket(int port, int backlog) | 创建绑定到指定端口的服务器套接字,并指定连接请求队列的最大长度 backlog 。 | |
public ServerSocket(int port, int backlog, InetAddress bindAddr) | 创建绑定到指定 IP 地址和端口的服务器套接字,指定 backlog 来定义连接请求的队列长度。 | |
public Socket(String host, int port)(客户端) | 创建一个客户端 Socket 并连接到指定的远程主机 host 和端口 port。 | |
绑定端口方法 | public void bind(SocketAddress endpoint)(客户端、服务端) | 将服务器套接字绑定到指定的 SocketAddress ,通常是 InetSocketAddress ,以监听特定端口。 |
public void bind(SocketAddress endpoint, int backlog) | 绑定到指定的 SocketAddress 并设置连接请求队列的最大长度 backlog ,队列满时拒绝新连接请求。 | |
监听客户端 | protected void listen(int backlog) | 将服务器套接字置于监听状态,backlog 参数指定允许等待连接的最大队列长度,超过该长度的连接请求将被拒绝。 |
接受客户端请求 | public Socket accept() throws IOException | 等待客户端的连接请求,接受成功后返回一个与客户端通信的新 Socket 对象。 |
尝试连接服务端socket | public void connect(SocketAddress endpoint, int timeout)(客户端) | 尝试连接到远程服务器指定的 SocketAddress 地址,timeout 参数指定连接超时时间(毫秒)。如果在超时时间内未能连接,则抛出异常。 |
数据输入输出 | InputStream getInputStream() | 获取 Socket 的字节输入流 |
OutputStream getOutputStream() | 获取 Socket 的字节输出流 | |
InetAddress getInetAddress() | 获取对端的 IP 地址 | |
InetAddress getPort() | 获取对端的端口号 |
4、核心源码
1、核心方法
客户端和服务端最重要的两个类是Socket和Server Socket,这俩一个代表tcp通信的客户端,一个代表服务端。从源码来看,ServerSocket和socket内部分别持有一个SocketImpl对象,用于将对应的方法代理给native方法。以服务端ServerSocket为例:
构造函数中通过调用setImpl()创建了一个SocketImpl实现类,所有的创建链接、读写数据系统交互的本地方法都是在实现类中调用,也就是将对应的方法代理给native方法。
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; } } |
setImpl();方法在JDK17与JDK8中的实现方式不一样。JDK13之前使用的是PlainSocketImpl这个实现类(阻塞式 I/O),JDK 13后,Socket 的连接过程通过 NioSocketImpl 实现,采用了 NIO(非阻塞 I/O)技术来实现高效的网络连接和数据传输。具体可以参考:JDK13新特性
private void setImpl() { SocketImplFactory factory = ServerSocket.factory; if (factory != null) { impl = factory.createSocketImpl(); } else { impl = SocketImpl.createPlatformSocketImpl(true); } } static <S extends SocketImpl & PlatformSocketImpl> S createPlatformSocketImpl(boolean server) { (默认传参false) if (USE_PLAINSOCKETIMPL) { return (S) new PlainSocketImpl(server); } else { return (S) new NioSocketImpl(server); } } |
创建对应的socket实现类以后,调用bind方法绑定端口与开启监听,真正的本地方法的调用都在:getImpl().bind(epoint.getAddress(), epoint.getPort())、getImpl().listen(backlog)中实现:
public void bind(SocketAddress endpoint, int backlog) throws IOException { if (backlog < 1) backlog = 50; try { @SuppressWarnings("removal") SecurityManager security = System.getSecurityManager(); if (security != null) security.checkListen(epoint.getPort()); getImpl().bind(epoint.getAddress(), epoint.getPort()); getImpl().listen(backlog); bound = true; } catch(SecurityException e) { bound = false; throw e; } catch(IOException e) { bound = false; throw e; } } |
static void bind(ProtocolFamily family, FileDescriptor fd, InetAddress addr, int port) throws IOException { boolean preferIPv6 = isIPv6Available() && (family != StandardProtocolFamily.INET); if (addr.isLinkLocalAddress()) { addr = IPAddressUtil.toScopedAddress(addr); } bind0(fd, preferIPv6, exclusiveBind, addr, port); } |
2、本地方法
JDK8源码中的路径:C:\jdk-8\src\solaris\native\java\net\PlainSocketImpl.c JNIEXPORT void JNICALL Java_java_net_PlainSocketImpl_socketBind(JNIEnv *env, jobject this, jobject iaObj, jint localport) {} |
3、linux是怎么处理链接的?
前面提到的核心Api中,有关建立socket连接的Api中都有一个参数叫做backlog,源码中这个参数如果不设置,系统默认值为50,那么这个参数到底是什么?
public ServerSocket(int port) throws IOException { this(port, 50, null); } public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException { setImpl(); } |
关于这个参数,只需要看懂下面这张图就明白了:
Linux内核协议栈为TCP连接管理使用两个队列,一个是SYN队列(半链接队列,用来保存处于SYN_SENT和SYN_RECV状态的请求),一个是accpetd队列(用来保存处于established状态,但是应用层没有调用accept取走的请求),这两个队列是内核实现的,当服务器绑定、监听了某个端口后,这个端口的SYN队列和ACCEPT队列就建立好了。如图,整个过程就是:
1. 当SYN包到达了服务器后,内核会把这一信息放到SYN队列(即未完成握手队列)中,同时回一个SYN+ACK包给客户端。
2. 一段时间后,客户端再次发来了针对服务器SYN包的ACK网络分组时,内核会把连接从SYN队列中取出,再把这个连接放到ACCEPT队列(即已完成握手队列)中。
3. 服务器在第3步调用accept时,其实就是直接从ACCEPT队列中取出已经建立成功的连接套接字而已。
那么问题来了,当这两个队列满了后,新的请求到达了又将发生什么?
- 对于SYN队列,若队列满,则会直接丢弃请求,即新的SYN网络分组会被丢弃,这个问题好解决,客户端接收不到回复,会再一次发送,然后服务端继续丢弃,知道队列有空闲的位置。而客户端如果一直接收不到回复,发几次之后就会停止。
- 对于ACCEPT队列的处理就有点复杂了,分两种情况:
-
- 如果server端设置了sysctl_tcp_abort_on_overflow,那么server会发送rst给client,并删除掉这个链接。默认情况下是不会设置的。
- 如果没有设置sysctl_tcp_abort_on_overflow ,server端只是标记连接请求块的acked标志,并且连接建立定时器,会遍历半连接表,重新发送synack,重复上面的过程,如果重传次数超过synack重传的阀值,会把该连接从半连接链表中直接删除。
5、demo验证
建立连接
- 1、服务端创建服务端套接字,并开启客户端监听与等待连接请求
- 2、客户端客户端套接字,尝试连接服务端
- 3、客户端调用private static native int connect0(boolean preferIPv6,
FileDescriptor fd,
InetAddress remote,
int remotePort) - 3次握手建立连接
发送数据
- 1、客户端调用OutputStreamWriter发送数据
- 2、服务端调用InputStreamReader接收数据
断开连接
- 手动使客户端关闭连接
- 4次挥手断开连接