本文通过一个典型的java server socket代码,逐层剖析其tcp协议的服务端建立的原理,其中会涉及到linux内核的实现,本文会以简单通俗的图形将其中原理展示给大家.
本文图解示例是作者结合jdk,linux源码,将server socket建立基本流程及各流程之间的衔接关系,状态变更过程展示给读者,如表达有误或表达不清楚的地方, 欢迎大家拍砖给出更珍贵的意见.
以下是java Server Socket代码的运行环境是:
public class MultiThreadServer implements Runnable {
private Socket socket;
MultiThreadServer(Socket sock) {
this.socket = sock;
}
public static void main(String args[])
throws Exception {
ServerSocket serverSocket = new ServerSocket(1234);
while (true) {
Socket sock = serverSocket.accept();
new Thread(new MultiThreadServer(sock)).start();
}
}
public void run() {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while(true){
String str = in.readLine();
if("END".equals(str)){
break;
}
System.out.println("recv"+str);
}
}
catch (IOException e) {
System.out.println(e);
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
tcp socket建立过程 简单地讲就是: 客户端与服务器各自建立数据结构来处理交互状态,为接下来数据传输作准备。
概要地讲就是: 服务器端建立一个专门侦听某端口的socket(一直是LISTEN状态).每次成功与客户端握手后,会创建一个针对当前客户的新socket,后续程序通过这个socket便可与客户端进行数据交互。
至于详细的原理过程要就见以下的图解示例:
我们知道java的socket实现是通过调用操作系统的socket api实现的,下面图表展示其调用关系(调用详细过程后面会有详细代码分析):
java端的代码相对较简单,我们接下来会以linux api的步骤来分析其原理
jvm中调用linux底层api: socket()函数时,linux执行的步骤如上所示.
1 创建socket结构体
2 创建tcp_sock结构体,刚创建完的tcp_sock的状态为:TCP_CLOSE
3 创建文件描述符与socket绑定
上图组件解释:
调用bind()方法时,linux内核执行步骤如上图所示.其主要步骤有:
1.将当前网络命名空间名和端口存到bhash()
其中步骤中提到的网络命名空间是虚拟化相关技术的知识点,这里我们可以忽略一下它.
上面步骤可以近似看成 取当前当前网络命名空间和端口联合作为key(实际是取网络命名空间和端口来计算出hashCode),存在bhash这个哈希表中.
上图展示listen函数背后原理,步骤如下:
1.调用listen方法
2.检查侦听端口是否存在bhash中
3.初始化csk_accept_queue
4.将tcp_sock指针存放到listening_hash表
其中:
sock.csk_accept_queue主要是存放已握手成功的客户端sock.后面accept方法会重点介绍这个queue
listening_hash: 以 net(当前网络命名空间)和端口作为key计算出hashCode.存在listening_hash表中
本方法执行完后,tcp_sock的状态会变为:TCP_LISTEN,一直到服务器关闭这个侦听socket之前,本tcp_sock的状态一直都是TCP_LISTEN
sock的状态为TCP_LISTEN后,服务器端便可开始接收来自客户端的connection.我们常听到的三次握手就是发生在listen()之后.
如上面两图所示,accept()分两部分讲解,第一部分是侦听tcp_sock的csk_accept_queue队列,如果本队列为空,则阻塞,步骤如下:
1.调用accept方法
2.创建socket(创建新的准备用于连接客户端的socket)
3.创建文件描述符
4.阻塞式等待(csk_accept_queue)获取sock
我们知道在listen阶段,会为侦听的sock初始化csk_accept_queue,此时这个queue为空,所以accept()方法会在此时阻塞住,直到后面有客户端成功握手后,这个queue才有sock.如果csk_accept_queue不为空,则返回一个sock.后续的逻辑如accept第二个图所示,其步骤如下:
5.取出sock
6.socket与sock互相关联
7.socket与文件描述符关联
8.将socket返回给线程
先看来自网络的一张经典三次握手流程图,本文接下来会重点将Server端的那两次握手的流程原理
接收SYN回复ACK的流程步骤如下:
1.接收SYN数据包,调用tcp_v4_rcv()
2.当前客户的sock是否存在ehash中(当前sock不存在ehash)
3.如不存在ehash,则在listening_hash中找
4.创建request_sock
5.将request_sock存在ehash中(将request_sock的状态置为TCP_NEW_SYN_RECV)
6.发SYN,回ACK
服务器接收ACK的步骤如下:
1.接收ACK包,调用tcp_v4_rcv()
2.当前客户的sock是否存在ehash中
3.创建sock
4.删除request_sock
5.将sock存放到ehash
6.将tcp_sock加到queue中
每个socket最终会绑定一个文件描述符
而服务器端的侦听socket是在最初socket建立时,就绑定的。
而针对客户端的socket是在三次握手成功后,accept()方法中作绑定的
服务器端在侦听socket的tcp_sock状态变迁为TCP_LISTEN后,便可以接收客户的连接请求,三次握手就发生在listen()之后
通常我们所讨论的socket状态,实际指的是其sock的状态,下面我为大家列出侦听sock和针对客户端Sock的状态
有些读者看了上面的图解服务器端Server socket后,还觉得不够过瘾,还想从代码层面了解从java代码到linux内核是如何一步步调用的,那接下来这部分就是为这些读者准备的.
ServerSocket serverSocket = new ServerSocket(1234);
从java角度看,上一行代码就是创建一个端口号为:1234的ServerSocket.
但从底层实现来讲,它包含了如下三个重要操作
socket创建
socket绑定
socket的侦听
接下来我们会讲实现过程
在linux中,我们经常听说一切皆文件,所以socket也是一种文件,在socket建立过程中,会与文件描述符绑定.接下来我们会分jdk,linux实现两部分来讲解socket的创建
java中socket的创建主要是调用native方法:PlainSocketImpl.socketCreate来实现的,其调用栈如下:
main:14, MultiThreadServer
->ServerSocket.
->ServerSocket.
->ServerSocket.bind()
->ServerSocket.getImpl
->ServerSocket.createImpl
->AbstractPlainSocketImpl.create
->PlainSocketImpl.socketCreate()
----- 以下为jvm的native 实现 ------
->PlainSocketImpl.c文件的Java_java_net_PlainSocketImpl_socketCreate方法
->jvm.cpp文件的JVM_Socket方法
-->os_linux.inline.hpp文件的os::socket方法
-->glibc下的socket()方法
创建socket时,并不需要任何ip地址与端口.它只会与文件描述符绑定.
用户空间下调用socket()方法来创建socket,最终会调用内核空间的SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol),其主要实现如下:
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
...
//创建一个socket struct
retval = sock_create(family, type, protocol, &sock);
...
//创建一个struct file,并与socket.file绑定,最终挂到当前进程的files_struct结构体下
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
struct socket *sock;
...
//分配一个inode并与socket关联
sock = sock_alloc();
...
//下面create是函数指针,指向的是inet_create()方法
//对socket作各种初始化,创建分配sock.....
//socket是负责对用户提供接口,和文件系统作关联的
//sock是负责向下对接内核网络协议.
//create函数指针指向inet_create()分配sock空间,刚合建的状态为SS_UNCONNECTED
//其中在sk_alloc()->sk_prot_alloc()
//在sock_init_data()->sk_set_socket()中通过sock->sk_socket=socket作关联,且将其状态设置为TCP_CLOSE
err = pf->create(net, sock, protocol, kern);
...
}
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile;
//从当前进程的下分配新的fd
int fd = get_unused_fd_flags(flags);
//创建file struct并初始化,且socket.file指向这个file.
newfile = sock_alloc_file(sock, flags, NULL);
//最终在__fd_install方法中使用fdtable->fd[fd]=file方式绑定
fd_install(fd, newfile);
}
上面创建socket时,提到了创建的socket没有与任何网卡ip地址或端口绑定,那读者可能会疑惑,那要这socket何用呢?好吧,这部分我就带大家了解一下绑定的过程
socket绑定的,其调用栈如下:
main:14, MultiThreadServer
->ServerSocket.
->ServerSocket.
->ServerSocket.bind
->AbstractPlainSocketImpl.bind
->PlainSocketImpl.socketBind
----- 以下为jvm的native 实现 ------
->PlainSocketImpl.c文件的Java_java_net_PlainSocketImpl_socketBind方法
->net_util_md.c文件的NET_Bind方法
->glibc的bind方法
如果我们主机有多张网卡,而我们程序只指定1234端口,而没有指定网卡的ip时,则默认绑定0.0.0.0的ip,即绑定主机的所有网卡
bind()是由glibc提供,最终调用内核的SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen),本方法主要是调用inet_bind()方法
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
...
//当前是tcp协议,所以get_port函数指针指向的是:inet_csk_get_port()
sk->sk_prot->get_port(sk, snum))
...
}
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
...
//下面取的是tcp全局的inet_hashinfo hashinfo
struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo;
//从hbash获取端口号对应哈系表表头
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
//遍历该哈希表,一旦该列表中有相同的端口号(已经被绑定了)则继续轮询下一个
inet_bind_bucket_for_each(tb, &head->chain)
//实现方法是inet_bind_bucket_create()
//根据port创建inet_bind_bucket,并加到head中
tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
net, head, port);
...
}
socket侦听会打开端口,接收来自网络的请求,我们常说的tcp三次握手就发生在这里.
jdk代码部分的调用链如下
main:14, MultiThreadServer
->ServerSocket.
->ServerSocket.
->ServerSocket.bind
->AbstractPlainSocketImpl.listen
->PlainSocketImpl.socketListen
----- 以下为jvm的native 实现 ------
->PlainSocketImpl.c文件的Java_java_net_PlainSocketImpl_socketListen方法
->jvm.cpp文件的JVM_Listen方法
-->os_linux.inline.hpp文件的os::listen方法
-->glibc下的listen()方法
用户调用listen()函数,最终是SYSCALL_DEFINE2(listen, int, fd, int, backlog)函数来处理.如果是tcp协议,主要逻辑是调用inet_listen(struct socket *sock, int backlog)方法来实现
int inet_listen(struct socket *sock, int backlog){
...
if (old_state != TCP_LISTEN) {
//启动监听
err = inet_csk_listen_start(sk, backlog);
}
...
}
int inet_csk_listen_start(struct sock *sk, int backlog)
{
...
//初始化一个空的icsk_accept_queue
reqsk_queue_alloc(&icsk->icsk_accept_queue);
//将socket设为TCP_LISTEN状态
sk_state_store(sk, TCP_LISTEN);
//检查端口是否冲突
if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
...
//指向inet_hash,最终会把当前的sock加入到listening_hash
err = sk->sk_prot->hash(sk);
...
}
...
}
int inet_hash(struct sock *sk)
{
if (sk->sk_state != TCP_CLOSE) {
...
//把当前的sock加入到listening_hash
err = __inet_hash(sk, NULL);
...
}
return err;
}
本文重点在于分析服务器端的原理,所以客户端的发送SYN的代码我们这里忽略不讲,直接讲3次握手中的服务器端的那两次握手
tcp接收数据包最终都会调用tcp_v4_rcv方法,tcp三次握手中的第一次是服务器端接收到客户端的SYN包,并进行处理及返回ACK
int tcp_v4_rcv(struct sk_buff *skb){
...
//根据情况返回当前服务器的sock还是专门处理当前IP,端口的sock
//因为这里是第一次接收到客户端的SYN,所以返回的是服务器的sock
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (sk->sk_state == TCP_LISTEN) {
ret = tcp_v4_do_rcv(sk, skb);
goto put_and_return;
}
...
}
//__inet_lookup_skb方法会调用__inet_lookup
static inline struct sock *__inet_lookup(...)
{
//根据五元信息在hashinfo->ehash中查找有没合适的sock
//五元信息是网络命名空间,源地址,源端口,目标地址,目标端口
sk = __inet_lookup_established(net, hashinfo, saddr, sport,
daddr, hnum, dif, sdif);
//如果在ebash中找不到,则在listening_hash中查找并返回服务器的sock
//注意这里是客户端第一次发SYN,所以在执行上面的__inet_lookup_established()方法,是查找不到sock,所以要执行下面这方法才能查找到,并返回当前服务器的sock
return __inet_lookup_listener(net, hashinfo, skb, doff, saddr,
sport, daddr, hnum, dif, sdif);
}
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb){
...
if (tcp_rcv_state_process(sk, skb)) {
rsk = sk;
goto reset;
}
...
}
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb){
...
switch (sk->sk_state) {
...
case TCP_LISTEN:
...
//conn_request函数指针指向的是tcp_v4_conn_request()
//而tcp_v4_conn_request()方法主要是调用了tcp_conn_request方法
acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
...
}
}
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb){
//分配一个描述请求的request_sock,且其ireq_state 设置为 TCP_NEW_SYN_RECV
req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
...
//将request_sock加到ehash中
inet_csk_reqsk_queue_hash_add(sk, req,
tcp_timeout_init((struct sock *)req));
//调用的是tcp_v4_send_synack方法
af_ops->send_synack(sk, dst, &fl, req, &foc,
!want_cookie ? TCP_SYNACK_NORMAL :
TCP_SYNACK_COOKIE);
...
}
static int tcp_v4_send_synack(const struct sock *sk, struct dst_entry *dst,
struct flowi *fl,
struct request_sock *req,
struct tcp_fastopen_cookie *foc,
enum tcp_synack_type synack_type){
...
//构造syn+ack包
skb = tcp_make_synack(sk, dst, req, foc, synack_type);
...
//生成校验码
__tcp_v4_send_check(skb, ireq->ir_loc_addr, ireq->ir_rmt_addr);
//创建ip包并发送
err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
ireq->ir_rmt_addr,
ireq_opt_deref(ireq));
err = net_xmit_eval(err);
}
int tcp_v4_rcv(struct sk_buff *skb){
...
//根据五元信息在hashinfo->ehash中查找合适的sock
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (sk->sk_state == TCP_NEW_SYN_RECV) {
//进行验证
nsk = tcp_check_req(sk, skb, req, false);
}
tcp_child_process(sk, nsk, skb);
...
}
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
bool fastopen){
...
//syn_recv_sock的实现方法是tcp_v4_syn_recv_sock
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL,
req, &own_req);
...
//插入accept队列
inet_csk_complete_hashdance(sk, child, req, own_req);
...
}
struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst,
struct request_sock *req_unhash,
bool *own_req){
...
//tcp_create_openreq_child创建用于客户端的sock,其inet_csk_clone()中会
//执行newsk->sk_state = TCP_SYN_RECV
newsk = tcp_create_openreq_child(sk, req, skb);
...
//从ehash中删除旧的sock,插入新的sock,具体实现见inet_ehash_insert
*own_req = inet_ehash_nolisten(newsk, req_to_sk(req_unhash));
...
}
int tcp_child_process(struct sock *parent, struct sock *child,
struct sk_buff *skb){
...
ret = tcp_rcv_state_process(child, skb);
...
}
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb){
...
switch (sk->sk_state) {
case TCP_SYN_RECV:
//将sock状态设为TCP_ESTABLISHED
tcp_set_state(sk, TCP_ESTABLISHED);
}
}
struct sock *inet_csk_complete_hashdance(struct sock *sk, struct sock *child,
struct request_sock *req, bool own_req)
{
if (own_req) {
//如果request_sock在ehash中没有删除,则删除,正常情况下inet_ehash_nolisten中已删除好了
inet_csk_reqsk_queue_drop(sk, req);
reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);
//添加到listen 的sock的accept队列中
if (inet_csk_reqsk_queue_add(sk, req, child))
return child;
}
/* Too bad, another child took ownership of the request, undo. */
bh_unlock_sock(child);
sock_put(child);
return NULL;
}
如果来自客户端的tcp三次握手成功,则通过本方法会返回一个新的连接socket对象,服务器可以通过这个socket与连接这个socket客户端作交互.注意:有多少客户成功连接上服务器端,服务器端就会创建多少socket与之对应,则这些socket还会与系统的文件描述符绑定.
MultiThreadServer.main
->ServerSocket.accept
->ServerSocket.implAccept
->AbstractPlainSocketImpl.accept
->PlainSocketImpl.socketAccept
----- 以下为jvm的native 实现 ------
->PlainSocketImpl.c文件的Java_java_net_PlainSocketImpl_socketAccept方法
->linux_close.c文件的NET_Accept方法
->glibc的accept方法
客户端调用accept()方法时,最终调用内核的SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,int __user *, upeer_addrlen, int, flags)
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen, int, flags){
//通过文件描述符获得socket结构
sock = sockfd_lookup_light(fd, &err, &fput_needed);
//申请一个新的socket结构
newsock = sock_alloc();
//申请新的文件描述符
` newfd = get_unused_fd_flags(flags);
//创建file struct
newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
//tcp 的accept函数指针指向的是inet_accept()方法
//而inet_accept()中主要实现逻辑是调用了;inet_csk_accept()
err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);
//在__fd_install方法中使用fdtable->fd[fd]=file方式绑定
fd_install(newfd, newfile);
}
int inet_accept(struct socket *sock, struct socket *newsock, int flags,
bool kern)
{
//调用具体协议的accept操作,并得到新的sock结构,accept指向inet_csk_accept
struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err, kern);
//设置socket与sock关系
sock_graft(sk2, newsock);
//设置socket状态
newsock->state = SS_CONNECTED;
err = 0;
release_sock(sk2);
}
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
...
//等待一个新的连接
error = inet_csk_wait_for_connect(sk, timeo);
//从icsk_accept_queue队列中移除sock
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
}
Thread.run
->MultiThreadServer.run
->BufferedReader.readLine
->BufferedReader.readLine
->BufferedReader.fill
->InputStreamReader.read
->StreamDecoder.read
->StreamDecoder.implRead
->StreamDecoder.readBytes
->SocketInputStream.read
->SocketInputStream.read
->SocketInputStream.socketRead
->SocketInputStream.socketRead0
----- 以下为jvm的native 实现 ------
->SocketInputStream.c文件的Java_java_net_SocketInputStream_socketRead0方法
->linux_close.c文件的NET_Read方法
->glibc的recv方法
本文主要讲Socket的建立,所以关于数据读取的分析不在本文讲述内容.
作者:
吴炼钿