注:本文分析基于3.10.0-693.el7内核版本,即CentOS 7.4
在分析connect()系统调用时,我们已经发送SYN报文,所以服务端就需要作出回应了。我们依然只分析TCP层的操作。SYN报文到达TCP层由tcp_v4_rcv()接管。
int tcp_v4_rcv(struct sk_buff *skb)
{
const struct iphdr *iph;
const struct tcphdr *th;
struct sock *sk;
int ret;
struct net *net = dev_net(skb->dev);
...
//checksum检查,其实也就是完整性校验
if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo))
goto csum_error;
th = tcp_hdr(skb);//获取TCP头部
iph = ip_hdr(skb);//获取ip头部
TCP_SKB_CB(skb)->seq = ntohl(th->seq);
TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
skb->len - th->doff * 4);
TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
TCP_SKB_CB(skb)->tcp_flags = tcp_flag_byte(th);
TCP_SKB_CB(skb)->tcp_tw_isn = 0;
TCP_SKB_CB(skb)->ip_dsfield = ipv4_get_dsfield(iph);
TCP_SKB_CB(skb)->sacked = 0;
//根据报文的源和目的地址在established哈希表以及listen哈希表中查找连接
//对于正要建立的连接,返回的就是listen哈希表的连接
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)
goto no_tcp_socket;
process:
//如果此时socket状态处于time_wait,那就进入对应的处理流程中
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
...
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
sk_mark_napi_id(sk, skb);//记录napi的id
skb->dev = NULL;
bh_lock_sock_nested(sk);
tcp_sk(sk)->segs_in += max_t(u16, 1, skb_shinfo(skb)->gso_segs);
ret = 0;
if (!sock_owned_by_user(sk)) {//如果sk没有被用户锁定,即没在使用
//检查是否需要先进入prequeue队列
if (!tcp_prequeue(sk, skb))
ret = tcp_v4_do_rcv(sk, skb);//进入到主处理函数
//如果用户正在使用,则数据包进入backlog中
//不太理解的是为什么limit入参是sk_rcvbuf和sk_sndbuf之和
} else if (unlikely(sk_add_backlog(sk, skb,
sk->sk_rcvbuf + sk->sk_sndbuf))) {
bh_unlock_sock(sk);
NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
goto discard_and_relse;
}
bh_unlock_sock(sk);
sock_put(sk);
return ret;
...
do_time_wait:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
inet_twsk_put(inet_twsk(sk));
goto discard_it;
}
if (skb->len < (th->doff << 2)) {
inet_twsk_put(inet_twsk(sk));
goto bad_packet;
}
if (tcp_checksum_complete(skb)) {
inet_twsk_put(inet_twsk(sk));
goto csum_error;
}
//处理在time_wait状态收到报文的情况
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
case TCP_TW_SYN: {
struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
&tcp_hashinfo,
iph->saddr, th->source,
iph->daddr, th->dest,
inet_iif(skb));
if (sk2) {
inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
inet_twsk_put(inet_twsk(sk));
sk = sk2;
goto process;
}
/* Fall through to ACK */
}
case TCP_TW_ACK:
tcp_v4_timewait_ack(sk, skb);
break;
case TCP_TW_RST:
tcp_v4_send_reset(sk, skb);
inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
inet_twsk_put(inet_twsk(sk));
goto discard_it;
case TCP_TW_SUCCESS:;
}
goto discard_it;
}
接收到SYN包后要查看下该报文是否之前已建立的连接,通过__inet_lookup_skb()查找是否有匹配的连接。
static inline struct sock *__inet_lookup_skb(struct inet_hashinfo *hashinfo,
struct sk_buff *skb,
const __be16 sport,
const __be16 dport)
{
//sk_buff结构体里有一个变量指向sock,即skb->sk
//但是对于尚未建立连接的skb来说,其sk变量为空,因此会走进__inet_lookup()
struct sock *sk = skb_steal_sock(skb);
const struct iphdr *iph = ip_hdr(skb);
if (sk)
return sk;
else
return __inet_lookup(dev_net(skb_dst(skb)->dev), hashinfo,
iph->saddr, sport,
iph->daddr, dport, inet_iif(skb));
}
static inline struct sock *__inet_lookup(struct net *net,
struct inet_hashinfo *hashinfo,
const __be32 saddr, const __be16 sport,
const __be32 daddr, const __be16 dport,
const int dif)
{
u16 hnum = ntohs(dport);
//查找established哈希表
struct sock *sk = __inet_lookup_established(net, hashinfo,
saddr, sport, daddr, hnum, dif);
//查找listen哈希表
return sk ? : __inet_lookup_listener(net, hashinfo, saddr, sport,
daddr, hnum, dif);
}
最终会在listen哈希表中找到该连接,也就是服务端的监听socket。
之后如果当前这个监听socket没有被使用,就会进入prequeue队列中处理,但是由于这是SYN报文,还没有进程接收数据,所以不会进入prequeue的真正处理中。
bool tcp_prequeue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
//如果设置了/proc/sys/net/ipv4/tcp_low_latency(低时延)参数,默认为0
//或者用户还没有调用接收函数接收数据,那么不使用prequeue队列
//ucopy.task会在接收数据函数recvmsg()中设置为接收数据的当前进程
//所以对于第一个SYN报文,会从以下分支返回
if (sysctl_tcp_low_latency || !tp->ucopy.task)
return false;
if (skb->len <= tcp_hdrlen(skb) &&
skb_queue_len(&tp->ucopy.prequeue) == 0)
return false;
if (likely(sk->sk_rx_dst))
skb_dst_drop(skb);
else
skb_dst_force_safe(skb);
//加入到prequeue队列尾部
__skb_queue_tail(&tp->ucopy.prequeue, skb);
tp->ucopy.memory += skb->truesize;
//如果prequeue队列长度大于socket连接的接收缓冲区,
//将prequeue中的数据报文转移到receive_queue中
if (tp->ucopy.memory > sk->sk_rcvbuf) {
struct sk_buff *skb1;
BUG_ON(sock_owned_by_user(sk));
//从prequeue中摘链
while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
sk_backlog_rcv(sk, skb1);//放入backlog中
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPPREQUEUEDROPPED);
}
tp->ucopy.memory = 0;
//如果prequeue中有报文了,那么唤醒睡眠的进程来收取报文
} else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {
//唤醒sk上睡眠的进程,这里只唤醒其中一个,避免惊群现象
//至于怎么唤醒,选择哪个唤醒,暂未研究
wake_up_interruptible_sync_poll(sk_sleep(sk),
POLLIN | POLLRDNORM | POLLRDBAND);
//没有ACK需要发送,重置延时ACK定时器
if (!inet_csk_ack_scheduled(sk))
inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,
(3 * tcp_rto_min(sk)) / 4,
TCP_RTO_MAX);
}
return true;
}
既然不会进入到prequeue队列中,那就进入tcp_v4_do_rcv()的处理,这是个主要的报文处理函数。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
struct sock *rsk;
...
//SYN报文走的是这里
if (sk->sk_state == TCP_LISTEN) {
//查找对应的半连接状态的socket
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
if (!nsk)
goto discard;
//可知,对于SYN报文,返回的还是入参sk,即nsk=sk
if (nsk != sk) {
sock_rps_save_rxhash(nsk, skb);
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
} else
sock_rps_save_rxhash(sk, skb);
//这里是除ESTABLISHED and TIME_WAIT状态外报文的归宿。。。
if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
rsk = sk;
goto reset;
}
return 0;
...
}
半连接状态socket通过tcp_v4_hnd_req()查找。
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
struct tcphdr *th = tcp_hdr(skb);
const struct iphdr *iph = ip_hdr(skb);
struct sock *nsk;
struct request_sock **prev;
/* Find possible connection requests. */
//查找半连接队列,对于SYN报文肯定找不到
struct request_sock *req = inet_csk_search_req(sk, &prev, th->source,
iph->saddr, iph->daddr);
if (req)
return tcp_check_req(sk, skb, req, prev, false);
//再一次查找established哈希表,以防在此期间重传过SYN报文且建立了连接
//对于SYN报文这里也是返回空的
nsk = inet_lookup_established(sock_net(sk), &tcp_hashinfo, iph->saddr,
th->source, iph->daddr, th->dest, inet_iif(skb));
if (nsk) {
if (nsk->sk_state != TCP_TIME_WAIT) {
bh_lock_sock(nsk);
return nsk;
}
inet_twsk_put(inet_twsk(nsk));
return NULL;
}
#ifdef CONFIG_SYN_COOKIES
if (!th->syn)
sk = cookie_v4_check(sk, skb, &(IPCB(skb)->opt));
#endif
//所以最终返回的还是原来的连接,即该函数对于SYN报文啥都没做
return sk;
}
接下来就是进入tcp_rcv_state_process()处理,这个函数处理绝大多数状态的报文处理。
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock *req;
int queued = 0;
bool acceptable;
u32 synack_stamp;
tp->rx_opt.saw_tstamp = 0;
switch (sk->sk_state) {
case TCP_CLOSE:
goto discard;
case TCP_LISTEN:
...
if (th->syn) {//LISTEN状态收到SYN报文
if (th->fin)
goto discard;
//这里其实就是将请求放入半连接队列,必要时启动SYNACK定时器
if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
return 1;
kfree_skb(skb);
return 0;
}
goto discard;
}
...
}
加入半连接队列通过icsk->icsk_af_ops->conn_request操作。我们知道icsk->icsk_af_ops指向ipv4_specific,
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.sk_rx_dst_set = inet_sk_rx_dst_set,
.conn_request = tcp_v4_conn_request,
...
};
所以加入半连接的操作就是由tcp_v4_conn_request()操刀。
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
/* Never answer to SYNs send to broadcast or multicast */
if (skb_rtable(skb)->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST))
goto drop;
return tcp_conn_request(&tcp_request_sock_ops,
&tcp_request_sock_ipv4_ops, sk, skb);
drop:
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
return 0;
}
tcp_v4_conn_request()对tcp_conn_request()做了一个简单的封装。
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)
{
struct tcp_options_received tmp_opt;
struct request_sock *req;
struct tcp_sock *tp = tcp_sk(sk);
struct dst_entry *dst = NULL;
__u32 isn = TCP_SKB_CB(skb)->tcp_tw_isn;
bool want_cookie = false, fastopen;
struct flowi fl;
struct tcp_fastopen_cookie foc = { .len = -1 };
int err;
//如果开启了syncookies选项,/proc/sys/net/ipv4/
if ((sysctl_tcp_syncookies == 2 ||
//或者此时半连接队列已经满了
//同时isn不是由tcp_timewait_state_process()函数选择
//那么判断是否需要发送syncookie
inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
//不需要发送syncookies就直接丢弃报文
if (!want_cookie)
goto drop;
}
//如果全连接队列满了,同时半连接队列里尚未重传过的SYN报文个数大于1
//那么就直接丢弃报文
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
//都没问题的话,那就分配一个request_sock,表示一个请求
//这个内存分配是从tcp的slab中分配的
req = inet_reqsk_alloc(rsk_ops);
if (!req)
goto drop;
inet_rsk(req)->ireq_family = sk->sk_family;
//af_ops即为tcp_request_sock_ipv4_ops,这个结构体比较重要,请留意
tcp_rsk(req)->af_specific = af_ops;
tcp_clear_options(&tmp_opt);
tmp_opt.mss_clamp = af_ops->mss_clamp;
tmp_opt.user_mss = tp->rx_opt.user_mss;
//分析该请求的tcp各个选项,比如时间戳、窗口大小、快速开启等选项
tcp_parse_options(skb, &tmp_opt, 0, want_cookie ? NULL : &foc);
if (want_cookie && !tmp_opt.saw_tstamp)
tcp_clear_options(&tmp_opt);
tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;//记录时间戳选项开启情况
//将刚才分析的请求的TCP选项记录到刚刚分配的request_sock中,即req中
tcp_openreq_init(req, &tmp_opt, skb);
af_ops->init_req(req, sk, skb);
if (security_inet_conn_request(sk, skb, req))
goto drop_and_free;
//如果不需要发送syncookies
//同时isn不是由tcp_timewait_state_process()函数选择
if (!want_cookie && !isn) {
//如果开启了time_wait状态连接快速回收
//即设置/proc/sys/net/ipv4/tcp_tw_recycle
if (tcp_death_row.sysctl_tw_recycle) {
bool strict;
//查找路由
dst = af_ops->route_req(sk, &fl, req, &strict);
if (dst && strict &&
//主要用于判断是否会和该IP的旧连接冲突
//这里就涉及到nat环境下丢包的问题
!tcp_peer_is_proven(req, dst, true, tmp_opt.saw_tstamp)) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
goto drop_and_release;
}
}
/* Kill the following clause, if you dislike this way. */
//如果没有开启syncookies选项
else if (!sysctl_tcp_syncookies &&
//同时,半连接队列长度已经大于syn backlog队列的3/4
(sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
(sysctl_max_syn_backlog >> 2)) &&
//并且当前连接和旧连接有冲突
!tcp_peer_is_proven(req, dst, false, tmp_opt.saw_tstamp)) {
//很有可能遭受synflood 攻击
pr_drop_req(req, ntohs(tcp_hdr(skb)->source), rsk_ops->family);
goto drop_and_release;
}
//生成随机报文序列号
isn = af_ops->init_seq(skb);
}
if (!dst) {
dst = af_ops->route_req(sk, &fl, req, NULL);
if (!dst)
goto drop_and_free;
}
tcp_ecn_create_request(req, skb, sk, dst);
//如果要发送syncookies,那就发送
if (want_cookie) {
isn = cookie_init_sequence(af_ops, sk, skb, &req->mss);
req->cookie_ts = tmp_opt.tstamp_ok;
if (!tmp_opt.tstamp_ok)
inet_rsk(req)->ecn_ok = 0;
}
tcp_rsk(req)->snt_isn = isn;
tcp_openreq_init_rwin(req, sk, dst);
fastopen = !want_cookie && tcp_try_fastopen(sk, skb, req, &foc, dst);
//这里便是调用tcp_v4_send_synack()发送SYNACK报文了
err = af_ops->send_synack(sk, dst, &fl, req,
skb_get_queue_mapping(skb), &foc);
if (!fastopen) {
if (err || want_cookie)
goto drop_and_free;
tcp_rsk(req)->listener = NULL;
//发送报文后将该请求加入半连接队列,同时启动SYNACK定时器
//调用inet_csk_reqsk_queue_hash_add()完成上述操作
af_ops->queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
}
return 0;
...
}
要加入半连接队列首先要创建一个request_sock,用于表示客户端发起的请求,然后是做一些初始化,其中req->ts_recent后续会用到多次,这个变量表示的就是对端发送报文的时间(前提是对端开启了时间戳选项)。
static inline void tcp_openreq_init(struct request_sock *req,
struct tcp_options_received *rx_opt,
struct sk_buff *skb)
{
struct inet_request_sock *ireq = inet_rsk(req);
req->rcv_wnd = 0; /* So that tcp_send_synack() knows! */
req->cookie_ts = 0;
tcp_rsk(req)->rcv_isn = TCP_SKB_CB(skb)->seq;
tcp_rsk(req)->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
tcp_rsk(req)->snt_synack = tcp_time_stamp;
tcp_rsk(req)->last_oow_ack_time = 0;
req->mss = rx_opt->mss_clamp;
//如果对端开启时间戳,那么记录下这个时间,也就是对方发送SYN报文的时间
req->ts_recent = rx_opt->saw_tstamp ? rx_opt->rcv_tsval : 0;
ireq->tstamp_ok = rx_opt->tstamp_ok;//时间戳开启标志
ireq->sack_ok = rx_opt->sack_ok;
ireq->snd_wscale = rx_opt->snd_wscale;
ireq->wscale_ok = rx_opt->wscale_ok;
ireq->acked = 0;
ireq->ecn_ok = 0;
ireq->ir_rmt_port = tcp_hdr(skb)->source;
//目的端口,也就是当前服务端的监听端口
ireq->ir_num = ntohs(tcp_hdr(skb)->dest);
}
初始化完这个请求后就要看下这个请求是否有问题。主要检查的就是看是否会和当前ip的上次通讯有冲突。该操作通过tcp_peer_is_proven()检查。
#define TCP_PAWS_MSL 60 /* Per-host timestamps are invalidated
* after this time. It should be equal
* (or greater than) TCP_TIMEWAIT_LEN
* to provide reliability equal to one
* provided by timewait state.
*/
#define TCP_PAWS_WINDOW 1 /* Replay window for per-host
* timestamps. It must be less than
* minimal timewait lifetime.
*/
bool tcp_peer_is_proven(struct request_sock *req, struct dst_entry *dst,
bool paws_check, bool timestamps)
{
struct tcp_metrics_block *tm;
bool ret;
if (!dst)
return false;
rcu_read_lock();
tm = __tcp_get_metrics_req(req, dst);
if (paws_check) {
//如果当前ip的上次tcp通讯发生在60s内
if (tm && (u32)get_seconds() - tm->tcpm_ts_stamp < TCP_PAWS_MSL &&
//同时当前ip上次tcp通信的时间戳大于本次tcp,或者没有开启时间戳开关
//从这里看,快速回收打开选项就很容易导致nat环境丢包
((s32)(tm->tcpm_ts - req->ts_recent) > TCP_PAWS_WINDOW ||!timestamps))
ret = false;
else
ret = true;
} else {
if (tm && tcp_metric_get(tm, TCP_METRIC_RTT) && tm->tcpm_ts_stamp)
ret = true;
else
ret = false;
}
rcu_read_unlock();
return ret;
}
从这个冲突判断上看有这么几个条件:
- 同个ip的此次通信和上次通信间隔时间在60s(刚好等于time_wait状态默认持续时间)内
- 上次通信时间大于此次通信时间,或者没有开时间戳选项
所以,这样看来在nat环境下就很容易有问题。nat环境下的机器时间可能不统一,也就有可能出现某个机器先发的报文时间比较靠前,后面其他机器发的报文时间比较靠后,那个这个报文就会被丢弃,也就经常出现nat环境下有些机器无法连接网络的问题。
对于这种情况呢,我们注意到这是在快速回收选项开启的前提下才会检查,所以只要把快速回收选项tcp_tw_recycle关闭即可。因此nat环境最好不要打开这个选项。
一切OK的情况下,接着就该发送SYNACK报文了,通过af_ops->send_synack()。上面我们说过af_ops即为tcp_request_sock_ipv4_ops,
static const struct tcp_request_sock_ops tcp_request_sock_ipv4_ops = {
...
.route_req = tcp_v4_route_req,
.init_seq = tcp_v4_init_sequence,
.send_synack = tcp_v4_send_synack,
.queue_hash_add = inet_csk_reqsk_queue_hash_add,
};
因此,SYNACK报文就是通过tcp_v4_send_synack()发送的。
static int tcp_v4_send_synack(struct sock *sk, struct dst_entry *dst,
struct flowi *fl,
struct request_sock *req,
u16 queue_mapping,
struct tcp_fastopen_cookie *foc)
{
const struct inet_request_sock *ireq = inet_rsk(req);
struct flowi4 fl4;
int err = -1;
struct sk_buff * skb;
//获取路由
if (!dst && (dst = inet_csk_route_req(sk, &fl4, req)) == NULL)
return -1;
//准备synack报文,该报文使用的是用户的send buffer内存
skb = tcp_make_synack(sk, dst, req, foc);
if (skb) {
__tcp_v4_send_check(skb, ireq->ir_loc_addr, ireq->ir_rmt_addr);
skb_set_queue_mapping(skb, queue_mapping);
//传到IP层继续处理,组建ip头,然后发送报文
err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
ireq->ir_rmt_addr,
ireq->opt);
err = net_xmit_eval(err);
}
return err;
}
发送完SYNACK报文,接着就是将该连接放入半连接队列了,同时启动我们的SYNACK定时器。这一动作通过af_ops->queue_hash_add实现,由上面结构体可知,也就是调用inet_csk_reqsk_queue_hash_add()。
void inet_csk_reqsk_queue_hash_add(struct sock *sk, struct request_sock *req,
unsigned long timeout)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct listen_sock *lopt = icsk->icsk_accept_queue.listen_opt;
const u32 h = inet_synq_hash(inet_rsk(req)->ir_rmt_addr,
inet_rsk(req)->ir_rmt_port,
lopt->hash_rnd, lopt->nr_table_entries);
//添加到半连接队列
reqsk_queue_hash_req(&icsk->icsk_accept_queue, h, req, timeout);
//更新半连接队列统计信息,同时开启SYNACK定时器
inet_csk_reqsk_queue_added(sk, timeout);
}
有关SYNACK定时器的介绍,可以参看TCP SYNACK定时器梳理。
至此,TCP层的处理就算是结束了,后面进入IP层处理,然后通过网卡发送出去。
我们大概总结一下总体流程:
- 根据SYN报文查找到服务端的监听socket;
- 查找是否有对应的半连接socket(第一个SYN报文肯定是没有的);
- 接着检查半连接队列和全连接队列是否满了,满了就丢弃报文;
- 然后就是创建和初始化一个request_sock,表示这个请求;
- 检查该请求是否和同IP的上个连接冲突;
- 一起OK的情况下,发送SYNACK报文;
- 报文发送完,请求入半连接队列,开启SYNACK定时器。
不过,有个问题不知大家有没有发现,为什么在接收到SYN报文后socket的状态没有改变,变成SYN_RECV呢?理论上收到SYN报文后就应该进入SYN_RECV的,至少从netstat命令查看是这样,书上也都是这么说的,但是代码里确实没有这么做,不知道是怎么回事?虽然我后面知道其实是在服务端接收到第三次握手报文后才会进入SYN_RECV转态,然后转为ESTABLISHED。不解不解。。。