socket底层实现

 

1,socket类之间继承关系图java

2,服务端socket链接维护linux

管理客户链接请求的任务是由操做系统来完成的。操做系统把这些链接请求存储在一个先进先出的队列中。许多操做系统限定了队列的最大长度,通常为50。当队列中的链接请求达到了队列的最大容量时,服务器进程所在的主机会拒绝新的链接请求。只有当服务器进程经过ServerSocket的accept()方法从队列中取出链接请求,使队列腾出空位时,队列才能继续加入新的链接请求。
对于客户进程,若是它发出的链接请求被加入到服务器的队列中,就意味着客户与服务器的链接创建成功,客户进程从Socket构造方法中正常返回。若是客户进程发出的链接请求被服务器拒绝,Socket构造方法就会抛出ConnectionException。web

3,服务端socket建立过程源码分析面试

   

类定义
public class ServerSocket implements java.io.Closeable数组

ServerSocket 类的声明很简单,实现了 Closeable 接口,该接口只有一个close方法。安全

主要属性
private boolean created = false;
private boolean bound = false;
private boolean closed = false;
private Object closeLock = new Object();
private SocketImpl impl;
private boolean oldImpl = false;服务器

•created 表示是否已经建立了 SocketImpl 对象,ServerSocket 须要依赖该对象实现套接字操做。
•bound 是否已绑定地址和端口。
•closed 是否已经关闭套接字。
•closeLock 关闭套接字时用的锁。
•impl 真正的套接字实现对象。
•oldImpl 是否是使用旧的实现。网络

主要方法并发

构造函数框架

有五类构造函数,能够什么参数都不传,也能够传入 SocketImpl、端口、backlog和地址等。主要看一下最后一个构造函数,setImpl 方法用于设置实现对象,而后检查端口大小是否正确,检查 backlog 小于0就让它等于50,最后进行端口和地址绑定操做。
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;
}
}

setImpl方法

设置套接字实现对象,这里提供了工厂模式能够方便的对接其余的实现,而默认是没有工厂对象的,因此模式的实现为 SocksSocketImpl 对象。
private void setImpl() {
if (factory != null) {
impl = factory.createSocketImpl();
checkOldImpl();
} else {
impl = new SocksSocketImpl();
}
if (impl != null)
impl.setServerSocket(this);
}

createImpl方法

该方法用于建立套接字实现对象,若是实现对象为空则先调用setImpl方法设置一下,接着调用套接字实现对象的create方法建立套接字。
void createImpl() throws SocketException {
if (impl == null)
setImpl();
try {
impl.create(true);
created = true;
} catch (IOException e) {
throw new SocketException(e.getMessage());
}
}

create方法干了些啥?它的实现逻辑在 AbstractPlainSocketImpl 类中,这里会传入一个 boolean 类型的 stream 变量,这里其实用来标识是 udp 仍是 tcp 协议,stream 便是流,tcp是基于链接的,天然存在流的抽象。而 udp 是非链接的非流的。

两类链接是经过 boolean 类型来标识的,true 为 tcp,false 为 udp,再经过 socketCreate 方法传入到本地实现中,在此以前二者都会建立 FileDescriptor 对象做为套接字的引用,FileDescriptor 为文件描述符,能够用来描述文件、套接字和资源等。另外,udp 协议时还会经过 ResourceManager.beforeUdpCreate()来统计虚拟机 udp 套接字数量,超过指定最大值则会抛出异常,默认值为25。最后将套接字的 created 标识设为 true,对应 Java 中抽象的客户端套接字 Socket 对象和服务端套接字 ServerSocket 对象。
protected synchronized void create(boolean stream) throws IOException {
this.stream = stream;
if (!stream) {
ResourceManager.beforeUdpCreate();
fd = new FileDescriptor();
try {
socketCreate(false);
} catch (IOException ioe) {
ResourceManager.afterUdpClose();
fd = null;
throw ioe;
}
} else {
fd = new FileDescriptor();
socketCreate(true);
}
if (socket != null)
socket.setCreated();
if (serverSocket != null)
serverSocket.setCreated();
}

往下看上面调用的socketCreate方法的逻辑,判断文件描述符不能为空,再调用本地socket0方法,最后将获得的句柄关联到文件描述符对象上。
void socketCreate(boolean stream) throws IOException {
if (fd == null)
throw new SocketException("Socket closed");

int newfd = socket0(stream, false /*v6 Only*/);

    fdAccess.set(fd, newfd);
}

static native int socket0(boolean stream, boolean v6Only) throws IOException;

接着看本地方法socket0的实现,逻辑为:
1.经过调用NET_Socket函数建立套接字句柄,其中经过 Winsock 库的 socket函数建立句柄,而且经过SetHandleInformation函数设置句柄的继承标志。这里能够看到根据 stream 标识对应的类别为SOCK_STREAM和 SOCK_DGRAM。若是句柄是无效的则抛出 create 异常。
2.而后经过setsockopt函数设置套接字的选项值,若是发生错误则抛出 create 异常。
3.最后再次经过SetHandleInformation设置句柄的继承标志,返回句柄。
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_socket0
(JNIEnv env, jclass clazz, jboolean stream, jboolean v6Only /unused*/) {
int fd, rv, opt=0;

fd = NET_Socket(AF_INET6, (stream ? SOCK_STREAM : SOCK_DGRAM), 0);
if (fd == INVALID_SOCKET) {
    NET_ThrowNew(env, WSAGetLastError(), "create");
    return -1;
}

rv = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *) &opt, sizeof(opt));
if (rv == SOCKET_ERROR) {
    NET_ThrowNew(env, WSAGetLastError(), "create");
}

SetHandleInformation((HANDLE)(UINT_PTR)fd, HANDLE_FLAG_INHERIT, FALSE);

return fd;

}

int NET_Socket (int domain, int type, int protocol) {
SOCKET sock;
sock = socket (domain, type, protocol);
if (sock != INVALID_SOCKET) {
SetHandleInformation((HANDLE)(uintptr_t)sock, HANDLE_FLAG_INHERIT, FALSE);
}
return (int)sock;
}

bind方法

该方法用于将套接字绑定到指定的地址和端口上,若是 SocketAddress 为空,即表明地址和端口都不指定,此时系统会将套接字绑定到全部有效的本地地址,且动态生成一个端口。逻辑以下:
1.判断是否已关闭,关闭则抛SocketException("Socket is closed")。
2.判断是否已绑定,绑定则抛SocketException("Already bound")。
3.判断地址是否为空,为空则建立一个 InetSocketAddress,默认是全部有效的本地地址,对应的为0.0.0.0,而端口默认为0,由操做系统动态生成。
4.判断对象是否为 InetSocketAddress 类型,不是则抛IllegalArgumentException("Unsupported address type")。
5.判断地址是否已经有值了,没有则抛SocketException("Unresolved address")。
6.backlog 若是小于1则设为50。
7.经过安全管理器检查端口。
8.经过套接字实现对象调用bind和listen方法。
9.bound 标识设为 true。
public void bind(SocketAddress endpoint) throws IOException {
bind(endpoint, 50);
}

public void bind(SocketAddress endpoint, int backlog) throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!oldImpl && isBound())
throw new SocketException("Already bound");
if (endpoint == null)
endpoint = new InetSocketAddress(0);
if (!(endpoint instanceof InetSocketAddress))
throw new IllegalArgumentException("Unsupported address type");
InetSocketAddress epoint = (InetSocketAddress) endpoint;
if (epoint.isUnresolved())
throw new SocketException("Unresolved address");
if (backlog < 1)
backlog = 50;
try {
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;
}
}

套接字实现对象的bind方法会间接调用socketBind方法,逻辑以下:
1.获取本地文件描述符 nativefd。
2.判断地址是否为空。
3.调用bind0本地方法。
4.若是端口为0还会调用localPort0本地方法获取本地端口赋值给套接字实现对象的 localport 属性上,目的是获取操做系统动态生成的端口。
void socketBind(InetAddress address, int port) throws IOException {
int nativefd = checkAndReturnNativeFD();

if (address == null)
        throw new NullPointerException("inet address argument is null.");

    bind0(nativefd, address, port, exclusiveBind);
    if (port == 0) {
        localport = localPort0(nativefd);
    } else {
        localport = port;
    }

    this.address = address;
}

static native void bind0(int fd, InetAddress localAddress, int localport, boolean exclBind)

static native int localPort0(int fd) throws IOException;

bind0本地方法逻辑以下,
1.经过NET_InetAddressToSockaddr函数将 Java 层的 InetAddress 对象的属性值填充到 SOCKETADDRESS 联合体中,对应的都是 Winsock 库的结构体,目的便是为了填充好它们。
typedef union {
struct sockaddr sa;
struct sockaddr_in sa4;
struct sockaddr_in6 sa6;
} SOCKETADDRESS;

2.NET_WinBind函数的逻辑是先根据 exclBind 标识看是否须要独占端口,若是须要则经过 Winsock 库的setsockopt函数设置SO_EXCLUSIVEADDRUSE选型,在 Java 层中决定独不独占端口能够经过sun.net.useExclusiveBind参数来配置,默认状况下是独占的。接着,经过操做系统的bind函数完成绑定操做。
3.若是绑定失败则抛异常。
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_bind0
(JNIEnv *env, jclass clazz, jint fd, jobject iaObj, jint port,
jboolean exclBind)
{
SOCKETADDRESS sa;
int rv, sa_len = 0;

if (NET_InetAddressToSockaddr(env, iaObj, port, &sa,
                              &sa_len, JNI_TRUE) != 0) {
  return;
}

rv = NET_WinBind(fd, &sa, sa_len, exclBind);

if (rv == SOCKET_ERROR)
    NET_ThrowNew(env, WSAGetLastError(), "NET_Bind");

}

localPort0本地方法的实现主要是先经过 Winsock 库的getsockname函数获取套接字地址,而后经过ntohs函数将网络字节转成主机字节并转为 int 型。
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_localPort0
(JNIEnv *env, jclass clazz, jint fd) {
SOCKETADDRESS sa;
int len = sizeof(sa);

if (getsockname(fd, &sa.sa, &len) == SOCKET_ERROR) {
    if (WSAGetLastError() == WSAENOTSOCK) {
        JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
                "Socket closed");
    } else {
        NET_ThrowNew(env, WSAGetLastError(), "getsockname failed");
    }
    return -1;
}
return (int) ntohs((u_short)GET_PORT(&sa));

}

套接字实现对象的listen方法会间接调用socketListen方法,逻辑比较简单,获取本地的文件描述符而后调用listen0本地方法。能够看到本地方法很简单,仅仅是调用了 Winsock 库的listen函数来完成监听操做。
void socketListen(int backlog) throws IOException {
int nativefd = checkAndReturnNativeFD();

listen0(nativefd, backlog);
}

static native void listen0(int fd, int backlog) throws IOException;

JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_listen0
(JNIEnv *env, jclass clazz, jint fd, jint backlog) {
if (listen(fd, backlog) == SOCKET_ERROR) {
NET_ThrowNew(env, WSAGetLastError(), "listen failed");
}
}

 

socket系统调用listen只被tcp 服务器使用,他作两件事:
1. 将未连接的套接口转换为被动套接口,指示内核接受向此套接口的链接请求,调用此系统调用后tcp 状态机有close转换到listen.
2.第二个参数制定了内核为此套接口排队的最大链接个数。
关于第二个参数,对于给定的监听套接口,内核要维护两个队列,未连接队列和已链接队列,根据tcp 三路握手过程当中三个分节来分隔这两个队列。
  服务器处于listen状态时收到客户端syn 分节(connect)时在未完成队列中建立一个新的条目,而后用三路握手的第二个分节即服务器的syn 响应及对客户端syn的ack,此条目在第三个分节到达前(客户端对服务器syn的ack)一直保留在未完成链接队列中,若是三路握手完成,该条目将从未完成链接队列搬到已完成链接队列尾部。当进程调用accept时,从已完成队列中的头部取出一个条目给进程,当已完成队列为空时进程将睡眠,直到有条目在已完成链接队列中才唤醒。
 backlog被规定为两个队列总和的最大值,大多数实现默认值为5,但在高并发web服务器中此值显然不够,lighttpd中此值达到128*8.须要设置此值更大一些的缘由是未完成链接队列的长度可能由于客户端SYN的到达及等待三路握手第三个分节的到达延时而增大。
当客户端发起connect而致使发送syn分节给服务器端握手,若是这时两个队列都是满的,tcp就忽略此分节,而且不发RST,这将致使客户端TCP重发SYN(超时),服务器端忽略syn而不发RST响应的缘由是若是发RST ,客户端connect将当即返回错误,强制客户端进程处理这种状况,而不是让tcp的正常重传机制来处理。实际上全部源自Berkeley的实现都是忽略新的SYN分节。
还有,backlog为0 时在linux上代表润许不受限制的链接数,这是一个缺陷,由于它可能会致使SYN Flooding(拒绝服务型攻击), 下一篇文章会简单解释。

 

linux 系统tcp /ip协议栈有个选项能够设置未连接队列大小

tcp_max_syn_backlog

 

cat /proc/sys/net/ipv4/tcp_max_syn_backlog 
1024

accept方法

该方法用于接收套接字链接,套接字开启监听后会阻塞等待套接字链接,一旦有链接可接收了则经过该方法进行接收操做。逻辑为,
1.判断套接字是否已经关闭。
2.判断套接字是否已经绑定。
3.建立 Socket 对象,并调用implAccept方法,
4.返回 Socket 对象。
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;
}

在这里给你们提供一个java进阶的学习交流平台

◾具备1-5工做经验的,面对目前流行的技术不知从何下手,须要突破技术瓶颈的能够加群。

◾在公司待久了,过得很安逸,但跳槽时面试碰壁。须要在短期内进修、跳槽拿高薪的能够加群。

◾若是没有工做经验,但基础很是扎实,对java工做机制,经常使用设计思想,经常使用java开发框架掌握熟练的能够加群。

◾731661047

implAccept方法逻辑为,
1.传入的 Socket 对象里面的套接字实现若是为空,则经过setImpl方法设置套接字实现,若是非空就执行reset操做。
2.调用套接字实现对象的accept方法完成接收操做,作这一步是由于咱们的 Socket 对象里面的 SocketImpl 对象还差操做系统底层的套接字对应的文件描述符。
3.调用安全管理器检查权限。
4.获得完整的 SocketImpl 对象,赋值给 Socket 对象,而且调用postAccept方法将 Socket 对象设置为已建立、已链接、已绑定。
protected final void implAccept(Socket s) throws IOException {
SocketImpl si = null;
try {
if (s.impl == null)
s.setImpl();
else {
s.impl.reset();
}
si = s.impl;
s.impl = null;
si.address = new InetAddress();
si.fd = new FileDescriptor();
getImpl().accept(si);

SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkAccept(si.getInetAddress().getHostAddress(),
                                 si.getPort());
        }
    } catch (IOException e) {
        if (si != null)
            si.reset();
        s.impl = si;
        throw e;
    } catch (SecurityException e) {
        if (si != null)
            si.reset();
        s.impl = si;
        throw e;
    }
    s.impl = si;
    s.postAccept();
}

套接字实现对象的accept方法主要调用以下的socketAccept方法,逻辑为,
1.获取操做系统的文件描述符。
2.SocketImpl 对象为空则抛出NullPointerException("socket is null")。
3.若是 timeout 小于等于0则直接调用本地accept0方法,一直阻塞。
4.反之,若是 timeout 大于0,即设置了超时,那么会先调用configureBlocking本地方法,该方法用于将指定套接字设置为非阻塞模式。接着调用waitForNewConnection本地方法,若是在超时时间内能获取到新的套接字,则调用accept0方法获取新套接字的句柄,获取成功后再次调用configureBlocking本地方法将新套接字设置为阻塞模式。最后,若是非阻塞模式失败了,则将原来的套接字设置会紫塞模式,这里使用了 finally,因此能保证就算发生异常也能被执行。
5.最后将获取到的新文件描述符赋给 SocketImpl 对象,同时也将远程端口、远程地址、本地端口等都赋给它相关变量。
void socketAccept(SocketImpl s) throws IOException {
int nativefd = checkAndReturnNativeFD();
if (s == null)
throw new NullPointerException("socket is null");
int newfd = -1;
InetSocketAddress[] isaa = new InetSocketAddress[1];
if (timeout <= 0) {
newfd = accept0(nativefd, isaa);
} else {
configureBlocking(nativefd, false);
try {
waitForNewConnection(nativefd, timeout);
newfd = accept0(nativefd, isaa);
if (newfd != -1) {
configureBlocking(newfd, true);
}
} finally {
configureBlocking(nativefd, true);
}
}
fdAccess.set(s.fd, newfd);
InetSocketAddress isa = isaa[0];
s.port = isa.getPort();
s.address = isa.getAddress();
s.localport = localport;
}

configureBlocking本地方法逻辑很简单,以下,核心就是经过调用 Winsock 库的ioctlsocket函数来设置套接字为阻塞仍是非阻塞,根据 blocking 标识。
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_configureBlocking
(JNIEnv *env, jclass clazz, jint fd, jboolean blocking) {
u_long arg;
int result;

if (blocking == JNI_TRUE) {
    arg = SET_BLOCKING;    // 0
} else {
    arg = SET_NONBLOCKING;   // 1
}

result = ioctlsocket(fd, FIONBIO, &arg);
if (result == SOCKET_ERROR) {
    NET_ThrowNew(env, WSAGetLastError(), "configureBlocking");
}

}

waitForNewConnection本地方法逻辑以下,核心是经过 Winsock 库的select函数来实现超时的功能,它会等待 timeout 时间看指定的文件描述符是否有活动,超时了的话则会返回0,此时向 Java 层抛出 SocketTimeoutException 异常。而若是返回了-1则表示套接字已经关闭了,抛出 SocketException 异常。若是返回-2则抛出 InterruptedIOException。
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_waitForNewConnection
(JNIEnv *env, jclass clazz, jint fd, jint timeout) {
int rv;

rv = NET_Timeout(fd, timeout);
if (rv == 0) {
    JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
                    "Accept timed out");
} else if (rv == -1) {
    JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", "socket closed");
} else if (rv == -2) {
    JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException",
                    "operation interrupted");
}

}

JNIEXPORT int JNICALL
NET_Timeout(int fd, long timeout) {
int ret;
fd_set tbl;
struct timeval t;
t.tv_sec = timeout / 1000;
t.tv_usec = (timeout % 1000) * 1000;
FD_ZERO(&tbl);
FD_SET(fd, &tbl);
ret = select (fd + 1, &tbl, 0, 0, &t);
return ret;
}

accept0本地方法实现逻辑为,
1.经过C语言的memset函数将 SOCKETADDRESS 联合体对应的结构体内的值设置为0。
2.经过 Winsock 库的accept函数获取套接字地址。
3.判断接收的套接字描述符是否无效,分别可能抛 InterruptedIOException 或 SocketException 异常。
4.经过SetHandleInformation函数设置句柄的继承标志。
5.NET_SockaddrToInetAddress函数用于将获得的套接字转换成 Java 层的 InetAddress 对象。
6.将生成的 InetAddress 对象用于生成 Java 层的 InetSocketAddress 对象。
7.赋值给 Java 层的 InetSocketAddress 数组对象。
8.返回新接收的套接字的文件描述符。
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_accept0
(JNIEnv *env, jclass clazz, jint fd, jobjectArray isaa) {
int newfd, port=0;
jobject isa;
jobject ia;
SOCKETADDRESS sa;
int len = sizeof(sa);

memset((char *)&sa, 0, len);
newfd = accept(fd, &sa.sa, &len);

if (newfd == INVALID_SOCKET) {
    if (WSAGetLastError() == -2) {
        JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException",
                        "operation interrupted");
    } else {
        JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
                        "socket closed");
    }
    return -1;
}

SetHandleInformation((HANDLE)(UINT_PTR)newfd, HANDLE_FLAG_INHERIT, 0);

ia = NET_SockaddrToInetAddress(env, &sa, &port);
isa = (*env)->NewObject(env, isa_class, isa_ctorID, ia, port);
(*env)->SetObjectArrayElement(env, isaa, 0, isa);

return newfd;

原文连接:https://juejin.im/post/5abae0895188255c566878d2?utm_source=tuicool&utm_medium=referral

相关文章
相关标签/搜索