图解Java服务端Socket建立原理

1.前言

1.1 目标

本文通过一个典型的java server socket代码,逐层剖析其tcp协议的服务端建立的原理,其中会涉及到linux内核的实现,本文会以简单通俗的图形将其中原理展示给大家.

1.2 读者收获知识

  • 理解socket与文件描述符绑定关系以及绑定的时机
  • 客户端与服务器端三次握手的发生时机
  • socket状态变化过程

1.3 读者收获不到的知识

  • windows 服务端socket的建立过程
  • 客户端的socket建立过程

1.4 欢迎读者指出不足

本文图解示例是作者结合jdk,linux源码,将server socket建立基本流程及各流程之间的衔接关系,状态变更过程展示给读者,如表达有误或表达不清楚的地方, 欢迎大家拍砖给出更珍贵的意见.

2.代码示例

以下是java Server Socket代码的运行环境是:

  • Ubuntu18 (内核版本为Linux4.15)
  • JDK 8.0
    后面的图解及代码剖析都是基于上面的linux4.15,jdk8来讲解.
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();
   		}
   	}
   }
}

3.图解服务器端Socket建立

tcp socket建立过程 简单地讲就是: 客户端与服务器各自建立数据结构来处理交互状态,为接下来数据传输作准备。

概要地讲就是: 服务器端建立一个专门侦听某端口的socket(一直是LISTEN状态).每次成功与客户端握手后,会创建一个针对当前客户的新socket,后续程序通过这个socket便可与客户端进行数据交互。

至于详细的原理过程要就见以下的图解示例:

3.1 java与linux api调用关系

我们知道java的socket实现是通过调用操作系统的socket api实现的,下面图表展示其调用关系(调用详细过程后面会有详细代码分析):
在这里插入图片描述
java端的代码相对较简单,我们接下来会以linux api的步骤来分析其原理

3.2 图解linux内核中socket建立

3.2.1 socket()

socket创建
jvm中调用linux底层api: socket()函数时,linux执行的步骤如上所示.
1 创建socket结构体
2 创建tcp_sock结构体,刚创建完的tcp_sock的状态为:TCP_CLOSE
3 创建文件描述符与socket绑定
上图组件解释:

  • socket: 用于服务器监听的socket,主要用于给用户提供接口,和文件关联.
  • tcp_sock:负责tcp及下的内核网络协议栈,这个结构体用户进程不感知.有时把它称为sock(sock 结构体可近似看成是tcp_sock结构体的父类).
    bhash,listening_hash,ehash可以近似看成像java的HashMap,后续会详细介绍其存放的对象

3.2.2 bind()

在这里插入图片描述
调用bind()方法时,linux内核执行步骤如上图所示.其主要步骤有:
1.将当前网络命名空间名和端口存到bhash()

其中步骤中提到的网络命名空间是虚拟化相关技术的知识点,这里我们可以忽略一下它.
上面步骤可以近似看成 取当前当前网络命名空间和端口联合作为key(实际是取网络命名空间和端口来计算出hashCode),存在bhash这个哈希表中.

3.2.3 listen()

在这里插入图片描述
上图展示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()之后.

3.2.4 accept()

在这里插入图片描述
在这里插入图片描述
如上面两图所示,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返回给线程

3.2.5 服务器与客户端的握手

在这里插入图片描述
先看来自网络的一张经典三次握手流程图,本文接下来会重点将Server端的那两次握手的流程原理

3.2.5.1 服务器接收SYN回复ACK

在这里插入图片描述
在这里插入图片描述
接收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

3.2.5.2 服务器接收ACK

在这里插入图片描述
在这里插入图片描述
服务器接收ACK的步骤如下:
1.接收ACK包,调用tcp_v4_rcv()
2.当前客户的sock是否存在ehash中
3.创建sock
4.删除request_sock
5.将sock存放到ehash
6.将tcp_sock加到queue中

4. 读者收获知识点

4.1 理解socket与文件描述符绑定关系以及绑定的时机

每个socket最终会绑定一个文件描述符
而服务器端的侦听socket是在最初socket建立时,就绑定的。
而针对客户端的socket是在三次握手成功后,accept()方法中作绑定的

4.2 客户端与服务器端三次握手的发生时机

服务器端在侦听socket的tcp_sock状态变迁为TCP_LISTEN后,便可以接收客户的连接请求,三次握手就发生在listen()之后

4.3 socket状态变化过程

通常我们所讨论的socket状态,实际指的是其sock的状态,下面我为大家列出侦听sock和针对客户端Sock的状态
在这里插入图片描述

5.服务器端Socket建立过程源代码分析

有些读者看了上面的图解服务器端Server socket后,还觉得不够过瘾,还想从代码层面了解从java代码到linux内核是如何一步步调用的,那接下来这部分就是为这些读者准备的.

5.1 new ServerSocket(1234)

ServerSocket serverSocket = new ServerSocket(1234);
从java角度看,上一行代码就是创建一个端口号为:1234的ServerSocket.
但从底层实现来讲,它包含了如下三个重要操作
socket创建
socket绑定
socket的侦听
接下来我们会讲实现过程

5.1.2 socket创建

在linux中,我们经常听说一切皆文件,所以socket也是一种文件,在socket建立过程中,会与文件描述符绑定.接下来我们会分jdk,linux实现两部分来讲解socket的创建

5.1.2.1 jdk代码中socket创建

java中socket的创建主要是调用native方法:PlainSocketImpl.socketCreate来实现的,其调用栈如下:
main:14, MultiThreadServer
->ServerSocket.<init>
	->ServerSocket.<init>
		->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地址与端口.它只会与文件描述符绑定.

5.1.2.2 linux系统代码中socket创建

用户空间下调用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);
}

5.1.3 socket绑定

上面创建socket时,提到了创建的socket没有与任何网卡ip地址或端口绑定,那读者可能会疑惑,那要这socket何用呢?好吧,这部分我就带大家了解一下绑定的过程

5.1.3.1 jdk代码中socket绑定实现

socket绑定的,其调用栈如下:
main:14, MultiThreadServer
->ServerSocket.<init>
	->ServerSocket.<init>
		->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,即绑定主机的所有网卡

5.1.3.2 linux系统代码中socket绑定

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);
	...
}

5.1.4 socket的侦听

socket侦听会打开端口,接收来自网络的请求,我们常说的tcp三次握手就发生在这里.

5.1.4.1 jdk代码中socket侦听

jdk代码部分的调用链如下
main:14, MultiThreadServer
->ServerSocket.<init>
	->ServerSocket.<init>
		->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()方法

5.1.4.2 linux系统代码中socket侦听

用户调用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;
}

5.1.5 tcp三次握手

本文重点在于分析服务器端的原理,所以客户端的发送SYN的代码我们这里忽略不讲,直接讲3次握手中的服务器端的那两次握手

5.1.5.1 tcp三次握手之接收SYN并发送ACK

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);


}

5.1.5.2tcp三次握手之接收ACK建立连接

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;
}

5.2 serverSocket.accept()

5.2.1 socket 的accept

如果来自客户端的tcp三次握手成功,则通过本方法会返回一个新的连接socket对象,服务器可以通过这个socket与连接这个socket客户端作交互.注意:有多少客户成功连接上服务器端,服务器端就会创建多少socket与之对应,则这些socket还会与系统的文件描述符绑定.

5.2.1.1 jdk代码中的accept

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方法

5.2.1.2 linux系统代码中socket的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;

}

5.3 in.readLine()

5.3.1 jdk代码中socket读取数据

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方法

5.2.1.2 linux系统代码中socket读取数据

本文主要讲Socket的建立,所以关于数据读取的分析不在本文讲述内容.

作者:
吴炼钿

发布了87 篇原创文章 · 获赞 42 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/vipshop_fin_dev/article/details/102966081
今日推荐