TCP/IP协议栈源代码分析

一、inet_init是如何被调用的?从start_kernel到inet_init调用路径

  • start_kernel 函数位于 init/main.c 文件中,是内核启动的入口函数。
asmlinkage __visible void __init start_kernel(void)
{
    // ...

    // 调用 rest_init 函数
    rest_init();

    // ...
}
  • rest_init 函数位于 kernel/init/main.c 文件中,完成一些初始化工作,并通过 kernel_thread 函数创建一个新的内核线程。
static void __init rest_init(void)
{
    // ...

    // 创建一个新的内核线程,并指定其执行函数为 kernel_init
    kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);

    // ...
}
  • kernel_init 函数位于 init/main.c 文件中,是内核初始化的第一个阶段。
static int __init kernel_init(void *unused)
{
    // ...

    // 调用 do_basic_setup 函数
    do_basic_setup();

    // ...

    // 调用 do_initcalls 函数执行内核初始化的第二个阶段
    do_initcalls();

    // ...
}
  • do_basic_setup 函数位于 init/main.c 文件中,完成一些基本的系统初始化工作,并调用 do_initcalls 函数执行内核初始化的第二个阶段。
static void __init do_basic_setup(void)
{
    // ...

    // 调用 do_initcalls 函数执行内核初始化的第二个阶段
    do_initcalls();

    // ...
}
  • do_initcalls 函数位于 init/main.c 文件中,会调用 do_one_initcall 函数多次,以执行一系列的初始化函数。
static void __init do_initcalls(void)
{
    // ...

    // 调用 do_one_initcall 函数,执行一系列的初始化函数
    do_one_initcall(inet_init);

    // ...
}
  • inet_init 函数位于 net/ipv4/af_inet.c 文件中,是网络子系统中 IPv4 协议族的初始化函数。它执行了一些与 IPv4 相关的初始化工作。
static int __init inet_init(void)
{
    // ...

    // 执行 IPv4 相关的初始化工作

    // ...

    return 0;
}

因此,start_kernel -> rest_init -> kernel_init -> do_basic_setup -> do_initcalls -> do_one_initcall -> inet_initinet_init 函数从 start_kernelinet_init 的调用路径。

二、跟踪分析TCP/IP协议栈如何将自己与上层套接口与下层数据链路层关联起来的?

在 Linux 内核中,TCP/IP 协议栈通过网络套接口(socket)与上层应用程序和下层数据链路层进行关联。

  • 上层套接口与协议关联(以 TCP 为例):
// 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

// 绑定套接字到本地地址和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(1234);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));

// 监听连接请求
listen(sockfd, SOMAXCONN);

socket 系统调用创建了一个 TCP 套接字,并返回相应的文件描述符。bind 系统调用将套接字绑定到本地地址和端口。之后,通过 listen 系统调用开始监听连接请求。

  • 数据传输和处理:
// 接受连接请求
struct sockaddr_in client_addr;
socklen_t client_addrlen = sizeof(client_addr);
int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);

// 发送和接收数据
char buffer[1024];
ssize_t num_bytes = recv(connfd, buffer, sizeof(buffer), 0);
send(connfd, buffer, num_bytes, 0);

accept 系统调用接受连接请求,并返回一个新的文件描述符 connfd,用于与客户端进行通信。通过 recvsend 系统调用,可以在套接字之间传输数据。

  • 下层数据链路层处理
// 发送数据帧
struct ethhdr eth_header;
struct iphdr ip_header;
struct tcphdr tcp_header;
// 构建以太网头部、IP 头部和 TCP 头部

struct sk_buff *skb = alloc_skb(total_len, GFP_KERNEL);
// 分配 sk_buff 结构体

// 将数据帧封装到 skb
memcpy(skb_put(skb, sizeof(eth_header)), &eth_header, sizeof(eth_header));
memcpy(skb_put(skb, sizeof(ip_header)), &ip_header, sizeof(ip_header));
memcpy(skb_put(skb, sizeof(tcp_header)), &tcp_header, sizeof(tcp_header));

// 发送 skb
dev_queue_xmit(skb);

在上述示例中,通过构建以太网头部、IP 头部和 TCP 头部,将数据帧封装到一个 sk_buff 结构体中。然后,使用 dev_queue_xmit 函数将 skb 发送到网络设备队列中,以便进一步处理和发送到物理链路。

三、TCP的三次握手源代码跟踪分析,跟踪找出设置和发送SYN/ACK的位置,以及状态转换的位置

  • 设置和发送 SYN/ACK 的位置:

在 TCP 的三次握手过程中,服务器端(被动打开方)负责设置和发送 SYN/ACK 报文。

// 文件:net/ipv4/tcp_ipv4.c
// 函数:tcp_v4_rcv
// 位置:约在 3500 行附近

// 处理接收到的 SYN 报文段
static void tcp_v4_rcv(struct sk_buff *skb)
{
    // ...

    // 检查报文段是否是 SYN 报文
    if (th->syn) {
        // ...

        // 创建一个新的连接请求
        struct tcp_sock *newtp = tcp_create_openreq_child(sk, skb);
        if (newtp) {
            // 设置新连接请求的状态和序列号等参数
            tcp_init_sock(newtp, req->rsk_listener);

            // 设置 SYN/ACK 报文的标志
            tcp_initialize_rcv_mss(newtp);
            tcp_synack_rtt_meas(newtp, req);

            // 构建 SYN/ACK 报文
            tcp_send_synack(newtp, req);

            // 发送 SYN/ACK 报文
            tcp_event_data_recv(newtp, skb);

            // ...
        }
    }

    // ...
}

在上述代码中,tcp_v4_rcv 函数用于处理接收到的 TCP 报文。当接收到的报文段的 SYN 标志被设置时,表示这是一个连接请求的 SYN 报文。在这种情况下,内核会根据接收到的报文创建一个新的连接请求,并通过调用 tcp_send_synack 函数设置并发送 SYN/ACK 报文。

  • 状态转换的位置:

在 TCP 的三次握手过程中,涉及到多个状态的转换,其中包括 SYN_SENT、SYN_RECV、ESTABLISHED 等。

// 文件:net/ipv4/tcp_ipv4.c
// 函数:tcp_v4_syn_recv_sock
// 位置:约在 3600 行附近

// 创建 SYN_RECV 状态的套接字
static struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
                                         struct request_sock *req,
                                         struct dst_entry *dst)
{
    // ...

    // 创建 SYN_RECV 状态的套接字
    newsk = tcp_create_openreq_child(sk, skb);
    if (newsk) {
        // 设置套接字的状态为 SYN_RECV
        newtp = tcp_sk(newsk);
        newtp->rx_opt.tstamp_ok = sysctl_tcp_timestamps;
        newtp->advmss = tcp_mtu_to_mss(newtp, dst_mtu(dst));

        // ...

        // 触发状态转换
        inet_csk(sk)->icsk_accept_queue.rskq_defer_accept = 1;
        inet_csk(newsk)->icsk_ulp_data = req->ulp_info;

        // ...
    }

    // ...

    return newsk;
}

在上述代码中,tcp_v4_syn_recv_sock函数用于创建 SYN_RECV 状态的套接字。在创建套接字后,通过设置套接字的状态和其他相关参数来触发状态转换。这里使用了 inet_csk(sk)->icsk_accept_queue.rskq_defer_accept 标志来延迟接受连接,从而将套接字状态从 SYN_SENT 转换为 SYN_RECV。

四、send在TCP/IP协议栈中的执行路径

send 函数用于将数据发送到 TCP/IP 协议栈中的 TCP 套接字

  • 用户层调用 send 函数:
// 文件:net/socket.c
// 函数:sock_sendmsg
// 位置:约在 3500 行附近

ssize_t sock_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
    // ...

    // 调用协议族的 sendmsg 函数
    ssize_t ret = sock->ops->sendmsg(sock, msg, len);

    // ...

    return ret;
}

sock_sendmsg 函数是用户层调用 send 函数的入口。它将调用协议族的 sendmsg 函数来处理数据的发送。

  • 调用 TCP 协议族的 sendmsg 函数:
// 文件:net/ipv4/tcp.c
// 函数:tcp_sendmsg
// 位置:约在 8000 行附近

ssize_t tcp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
    // ...

    // 将用户层的数据复制到发送缓冲区
    ret = memcpy_from_msg(data, msg, size);

    // ...

    // 调用核心的发送函数
    ret = tcp_sendmsg_locked(sk, msg, size);

    // ...

    return ret;
}

tcp_sendmsg 函数被调用来处理 TCP 协议族的数据发送。它首先将用户层的数据复制到发送缓冲区,然后调用核心的发送函数 tcp_sendmsg_locked 进行实际的发送操作。

  • 核心的发送函数 tcp_sendmsg_locked
// 文件:net/ipv4/tcp_output.c
// 函数:tcp_sendmsg_locked
// 位置:约在 1200 行附近

ssize_t tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
    // ...

    // 创建发送的 sk_buff
    skb = sock_alloc_send_skb(sk, size, msg->msg_flags & MSG_DONTWAIT, &err);
    if (!skb) {
        // 处理内存不足的情况
        goto failure;
    }

    // ...

    // 将用户层的数据复制到 sk_buff
    ret = memcpy_from_msg(skb_put(skb, size), msg, size);

    // ...

    // 调用 TCP/IP 协议栈的数据发送函数
    ret = tcp_transmit_skb(sk, skb, msg->msg_flags);

    // ...

    return ret;
}

tcp_sendmsg_locked 函数首先通过调用 sock_alloc_send_skb 函数创建一个发送的 sk_buff 结构体,然后将用户层的数据复制到 sk_buff 中。最后,它调用 TCP/IP 协议栈的数据发送函数 tcp_transmit_skb 来实际发送数据。

五、recv在TCP/IP协议栈中的执行路径

recv 函数用于从 TCP/IP 协议栈中的 TCP 套接字接收数据。

  • 用户层调用 recv 函数
// 文件:net/socket.c
// 函数:sock_recvmsg
// 位置:约在 4000 行附近

ssize_t sock_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
                     int flags)
{
    // ...

    // 调用协议族的 recvmsg 函数
    ssize_t ret = sock->ops->recvmsg(sock, msg, size, flags);

    // ...

    return ret;
}

sock_recvmsg 函数是用户层调用 recv 函数的入口。它将调用协议族的 recvmsg 函数来处理数据的接收。

调用 TCP 协议族的 recvmsg 函数:

// 文件:net/ipv4/tcp.c
// 函数:tcp_recvmsg
// 位置:约在 10000 行附近

ssize_t tcp_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
                    int flags)
{
    // ...

    // 调用核心的接收函数
    ret = tcp_recvmsg_locked(sk, msg, size, flags);

    // ...

    return ret;
}

tcp_recvmsg 函数被调用来处理 TCP 协议族的数据接收。它调用核心的接收函数 tcp_recvmsg_locked 进行实际的接收操作。

  • 核心的接收函数 tcp_recvmsg_locked
/ 文件:net/ipv4/tcp.c
// 函数:tcp_recvmsg_locked
// 位置:约在 9800 行附近

ssize_t tcp_recvmsg_locked(struct sock *sk, struct msghdr *msg, size_t size,
                           int flags)
{
    // ...

    // 从套接字接收数据
    copied = tcp_read_sock(sk, msg, size, flags);

    // ...

    return copied;
}

tcp_recvmsg_locked 函数通过调用 tcp_read_sock 函数从套接字接收数据。

  • 调用 tcp_read_sock 函数:
// 文件:net/ipv4/tcp.c
// 函数:tcp_read_sock
// 位置:约在 9100 行附近

static int tcp_read_sock(struct sock *sk, struct msghdr *msg, int len,
                         int nonblock)
{
    // ...

    // 从接收缓冲区读取数据
    copied = skb_copy_datagram_msg(skb, 0, msg, len);

    // ...

    return copied;
}

tcp_read_sock 函数通过调用 skb_copy_datagram_msg 函数从接收缓冲区读取数据,并将数据复制到 msg 结构体中。

六、路由表的结构和初始化过程

在 Linux 内核中,路由表用于确定将网络数据包从源地址传输到目标地址的路径。

  • 路由表的结构:
// 文件:include/net/route.h
// 位置:约在 200 行附近

struct rtable {
    // ...

    struct net_device *dst.dev;      // 目标设备
    __be32             rt_dst;       // 目标 IP 地址
    __be32             rt_gateway;   // 网关 IP 地址
    unsigned int       rt_pmtu;      // 最大传输单元
    // ...

    struct fib_nh_common nhc;
    // ...
};

rtable 结构体表示路由表的一条路由项。其中包含了目标设备、目标 IP 地址、网关 IP 地址、最大传输单元等信息。

  • 路由表的初始化过程:

// 文件:net/ipv4/route.c
// 函数:ip_route_input_noref
// 位置:约在 1000 行附近

struct rtable *ip_route_input_noref(struct net *net, __be32 daddr,
                                    __be32 saddr, u8 tos, struct net_device *dev)
{
    struct rtable *rt;

    // ...

    // 查找匹配的路由项
    rt = __ip_route_output_key(net, daddr, saddr, tos, dev);
    if (IS_ERR(rt)) {
        // 处理无法找到匹配路由项的情况
        return NULL;
    }

    // ...

    return rt;
}

在上述代码中,ip_route_input_noref 函数用于在输入路径上查找匹配的路由项。它调用 __ip_route_output_key 函数来查找路由表中与目标 IP 地址最匹配的路由项。

  • 查找路由项的具体实现:
// 文件:net/ipv4/route.c
// 函数:__ip_route_output_key
// 位置:约在 2000 行附近

struct rtable *__ip_route_output_key(struct net *net, __be32 daddr,
                                     __be32 saddr, u8 tos, struct net_device *dev)
{
    struct rtable *rt;

    // ...

    // 遍历路由缓存进行查找
    rt = ip_route_cache_lookup(net, daddr, saddr, tos, dev);
    if (rt) {
        // 找到匹配的路由项
        return rt;
    }

    // ...

    // 如果缓存中没有找到匹配项,则调用路由查找函数
    rt = ip_route_output_key_hash(net, daddr, saddr, tos, dev, 0, 0);

    // ...

    return rt;
}

在上述代码中,__ip_route_output_key 函数首先尝试从路由缓存中查找匹配的路由项。如果路由缓存中没有找到匹配项,则调用 ip_route_output_key_hash 函数进行实际的路由查找。

七、通过目的IP查询路由表的到下一跳的IP地址的过程

通过目的 IP 查询路由表获取下一跳 IP 地址的过程,可以通过以下步骤完成:

  1. 获取目的 IP 地址。

  2. 遍历路由表的条目,逐个比较目的 IP 地址与路由表中的目标地址。

  3. 对于每个路由表条目,检查是否匹配目的 IP 地址。匹配通常涉及子网掩码和最长前缀匹配。

  4. 如果匹配成功,获取路由表条目中的下一跳 IP 地址。

// 文件:net/ipv4/route.c
// 函数:ip_route_output_key_hash
// 位置:约在 2400 行附近

struct rtable *ip_route_output_key_hash(struct net *net, __be32 daddr,
                                         __be32 saddr, u8 tos,
                                         struct net_device *dev,
                                         int oif, int hash)
{
    // ...

    struct fib_result res;

    // 查询路由表
    fib_lookup(net, &fl, &res);

    // 检查是否匹配目的 IP 地址
    if (!res.fi) {
        // 没有匹配的路由项
        return NULL;
    }

    // 获取下一跳 IP 地址
    if (res.type == RTN_UNICAST) {
        // 单播路由
        struct fib_nh *nh = fib_info_nh(res.fi, res.table);
        next_hop = nh->nh_gw;
    } else {
        // 非单播路由,如多播、广播等
        // 处理方法根据需要进行调整
        // ...
    }

    // ...

    return rt;
}

在上述代码中,ip_route_output_key_hash 函数调用了 fib_lookup 函数查询路由表,并根据查询结果判断是否匹配到目的 IP 地址。如果匹配成功,根据路由类型获取下一跳 IP 地址。

八、ARP缓存的数据结构及初始化过程,包括ARP缓存的初始化

ARP(Address Resolution Protocol)缓存用于存储IP地址与MAC地址之间的映射关系,以便在网络通信中进行地址解析。下面是一个简化的示例源代码,展示了ARP缓存的数据结构及初始化过程。

  • ARP缓存的数据结构:
// 文件:include/net/arp.h
// 位置:约在 100 行附近

struct neighbour {
    // ...

    struct hh_cache hh;
    // ...

    u8 nud_state;
    // ...

    u8 ha[HARDWARE_ADDRESS_LEN];  // MAC地址
    // ...

    __be32 primary_key;  // IP地址
    // ...

    struct timer_list timer;
    // ...
};

在上述代码中,neighbour 结构体表示ARP缓存的一项。其中包含了MAC地址、IP地址等信息。

  • ARP缓存的初始化过程:
// 文件:net/ipv4/arp.c
// 函数:arp_init
// 位置:约在 200 行附近

void arp_init(void)
{
    // ...

    // 创建ARP缓存
    arp_tbl = neigh_table_init(&arp_tbl_parm);
    if (!arp_tbl) {
        printk(KERN_INFO "ARP: Failed to allocate memory for ARP table\n");
        return;
    }

    // ...

    return;
}

在上述代码中,arp_init 函数用于初始化ARP缓存。它调用 neigh_table_init 函数创建ARP缓存,并将参数传递给该函数进行初始化。

九、如何将IP地址解析出对应的MAC地址

在Linux内核中,将IP地址解析为对应的MAC地址主要通过ARP(Address Resolution Protocol)实现。大致的执行过程如下:

  • 用户空间调用套接字API发送一个IP数据包。

相关函数:sendto()connect()等。

相关文件:net/socket.cnet/ipv4/raw.c等。

  • 数据包到达网络层,内核检查目标IP地址是否在本地ARP缓存中,ARP缓存中存储了IP地址和对应的MAC地址。

相关数据结构:struct neighbourstruct neighbour_table等。

相关函数:neigh_lookup()等。

相关文件:net/core/neighbour.cinclude/net/neighbour.h等。

  • 如果目标IP地址不在本地ARP缓存中,内核创建并发送一个ARP请求消息。

相关函数:arp_create()arp_send()等。

相关文件:net/ipv4/arp.c等。

  • ARP请求消息封装到数据链路层帧中,通过网络接口发送出去。

相关函数:neigh_xmit()等。

相关文件:net/core/neighbour.cnet/ipv4/devinet.c等。

  • 目标主机收到ARP请求帧后,根据请求中的IP地址判断是否为自己的IP地址,并构建一个ARP响应消息发送回请求方。

相关函数:arp_rcv()等。

相关文件:net/ipv4/arp.c等。

  • 请求方收到ARP响应帧,提取MAC地址并将其存储到本地ARP缓存中。

相关函数:neigh_update()等。

相关文件:net/core/neighbour.cinclude/net/neighbour.h等。

  • 请求方将数据包发送到目标MAC地址。

相关函数:dev_queue_xmit()等。

相关文件:net/core/dev.c等。

十、跟踪TCP send过程中的路由查询和ARP解析的最底层实现

  • send过程中的路由查询 

这件事情主要由 ip_route_output_flow 函数完成,调用链 为:ip_route_output_flow->__ip_route_output_key->ip_route_output_key_hash->ip_route_output_key_hash_rcu

ip_route_output_key_hash_rcu 是 Linux 内核中用于根据给定的散列键值进行路由查找和输出的函数。它是 IPv4 路由子系统的一部分,用于确定给定目标 IP 地址的下一跳。

struct rtable *ip_route_output_key_hash_rcu(struct net *net, struct flowi4 *fl4, struct fib_result *res, const struct sk_buff *skb)
{
// ...
    err = fib_lookup(net, fl4, res, 0);
// ...
make_route:
  rth = __mkroute_output(res, fl4, orig_oif, dev_out, flags);
// ...
}

fib_lookup 是 Linux 内核中用于进行 FIB(Forwarding Information Base,转发信息库)查找的函数之一。它用于根据给定的目标地址查找匹配的路由表项。

static inline int fib_lookup(struct net *net, const struct flowi4 *flp, struct fib_result *res, unsigned int flags)
{  struct fib_table *tb;
// ...
  tb = fib_get_table(net, RT_TABLE_MAIN);
  if (tb)
    err = fib_table_lookup(tb, flp, res, flags | FIB_LOOKUP_NOREF);
// ...
}

  • ARP解析的最底层实现

__ipv4_neigh_lookup_noref 函数用于在本地 ARP 表中查找下一跳的 MAC 地址。该函数接收两个参数:dev 表示网络设备,dst 表示目标 IP 地址。它通过调用__neigh_lookup_noref 函数在 ARP 表中查找与目标 IP 地址匹配的邻居项(neighbour entry)。它接收邻居表 tbl、目标 IP 地址 pkey 和网络设备 dev 作为参数,返回一个指向邻居项结构体的指针。

若查找失败,则会调用 __neigh_create 函数。

void create_neigh_entry(struct net_device *dev, __be32 dst_ip, unsigned char *dst_mac)
{
    struct neighbour *neigh;

    // 创建邻居项
    neigh = __neigh_create(&arp_tbl, &dst_ip, dev, false);
    if (!neigh) {
        // 邻居项创建失败,处理错误情况
        return;
    }

    // 设置邻居项的 MAC 地址
    memcpy(neigh->ha, dst_mac, ETH_ALEN);

    // 将邻居项添加到邻居表
    neigh_add(neigh, false);
}

它接收网络设备 dev、目标 IP 地址 dst_ip 和目标 MAC 地址 dst_mac 作为参数。函数首先调用 __neigh_create 函数创建一个新的邻居项,并指定邻居表 arp_tbl、目标 IP 地址和网络设备。然后,它将目标 MAC 地址拷贝到邻居项的 ha 字段中,并通过调用 neigh_add 函数将邻居项添加到邻居表中。

在 __neigh_create 之后,会调用 neigh_output 发送网络包。neigh_output调用neigh_resolve_output。neigh_resolve_output 函数是用于发送网络包的关键函数之一,它用于解析目标 IP 地址的邻居项,并将网络包发送到相应的邻居。

neigh_resolve_output 函数的主要作用是将网络包发送给指定的邻居。它通常会执行以下步骤:

  1. 验证邻居项的状态:检查邻居项的状态,例如是否是有效的、是否已解析等。如果邻居项状态不满足发送条件,可能需要进行邻居探测或其他操作来解析邻居项。

  2. 设置网络包的目标 MAC 地址:从邻居项中获取目标 MAC 地址,并将其设置为网络包的目标 MAC 地址,以便正确路由和传递给下一跳。

  3. 发送网络包:将网络包发送到网络设备的发送队列,以便进行实际的数据传输。发送过程可能涉及网络设备驱动程序的调用,通过底层的网络协议栈将数据封装成数据帧并发送出去。

int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb) {
    if (neigh->nud_state != NUD_VALID) {
        // 邻居项状态不满足发送条件,可能需要进行邻居探测或其他操作
        // ...
    }

    // 设置网络包的目标 MAC 地址
    skb->mac_dest = neigh->ha;

    // 发送网络包
    int ret = dev_queue_xmit(skb);

    return ret;
}
  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
首先要理解基本的原理,2台电脑间实现TCP通讯,首先要建立起连接,在这里要提到服务器端与客户端,两个的区别通俗讲就是主动与被动的关系,两个人对话,肯定是先有人先发起会话,要不然谁都不讲,谈什么话题,呵呵!一样,TCPIP下建立连接首先要有一个服务器,它是被动的,它只能等待别人跟它建立连接,自己不会去主动连接,那客户端如何去连接它呢,这里提到2个东西,IP地址和端口号,通俗来讲就是你去拜访某人,知道了他的地址是一号大街2号楼,这个是IP地址,那么1号楼这么多门牌号怎么区分,嗯!门牌号就是端口(这里提到一点,我们访问网页的时候也是IP地址和端口号,IE默认的端口号是80),一个服务器可以接受多个客户端的连接,但是一个客户端只能连接一台服务器,在连接后,服务器自动划分内存区域以分配各个客户端的通讯,那么,那么多的客户端服务器如何区分,你可能会说,根据IP么,不是很完整,很简单的例子,你一台计算机开3个QQ,服务器怎么区分?所以准确的说是IP和端口号,但是客户端的端口号不是由你自己定的,是由计算机自动分配的,要不然就出现端口冲突了,说的这么多,看下面的这张图就简单明了了。 在上面这张图中,你可以理解为程序A和程序B是2个SOCKET程序,服务器端程序A设置端口为81,已接受到3个客户端的连接,计算机C开了2个程序,分别连接到E和D,而他的端口是计算机自动分配的,连接到E的端口为789,连接到D的为790。 了解了TCPIP通讯的基本结构后,接下来讲解建立的流程,首先声明一下我用的开发环境是Visual Studio2008版的,语言C#,组件System.Net.Sockets,流程的建立包括服务器端的建立和客户端的建立,如图所示: 二、实现: 1.客户端: 第一步,要创建一个客户端对象TcpClient(命名空间在System.Net.Sockets),接着,调用对象下的方法BeginConnect进行尝试连接,入口参数有4个,address(目标IP地址),port(目标端口号),requestCallback(连接成功后的返调函数),state(传递参数,是一个对象,随便什么都行,我建议是将TcpClient自己传递过去),调用完毕这个函数,系统将进行尝试连接服务器。 第二步,在第一步讲过一个入口参数requestCallback(连接成功后的返调函数),比如我们定义一个函数void Connected(IAsyncResult result),在连接服务器成功后,系统会调用此函数,在函数里,我们要获取到系统分配的数据流传输对象(NetworkStream),这个对象是用来处理客户端与服务器端数据传输的,此对象由TcpClient获得,在第一步讲过入口参数state,如果我们传递了TcpClient进去,那么,在函数里我们可以根据入口参数state获得,将其进行强制转换TcpClient tcpclt = (TcpClient)result.AsyncState,接着获取数据流传输对象NetworkStream ns = tcpclt.GetStream(),此对象我建议弄成全局变量,以便于其他函数调用,接着我们将挂起数据接收等待,调用ns下的方法BeginRead,入口参数有5个,buff(数据缓冲),offset(缓冲起始序号),size(缓冲长度),callback(接收到数据后的返调函数),state(传递参数,一样,随便什么都可以,建议将buff传递过去),调用完毕函数后,就可以进行数据接收等待了,在这里因为已经创建了NetworkStream对象,所以也可以进行向服务器发送数据的操作了,调用ns下的方法Write就可以向服务器发送数据了,入口参数3个,buff(数据缓冲),offset(缓冲起始序号),size(缓冲长度)。 第三步,在第二步讲过调用了BeginRead函数时的一个入口参数callback(接收到数据后的返调函数),比如我们定义了一个函数void DataRec(IAsyncResult result),在服务器向客户端发送数据后,系统会调用此函数,在函数里我们要获得数据流(byte数组),在上一步讲解BeginRead函数的时候还有一个入口参数state,如果我们传递了buff进去,那么,在这里我们要强制转换成byte[]类型byte[] data= (byte[])result.AsyncState,转换完毕后,我们还要获取缓冲区的大小int length = ns.EndRead(result),ns为上一步创建的NetworkStream全局对象,接着我们就可以对数据进行处理了,如果获取的length为0表示客户端已经断开连接。 具体实现代码,在这里我建立了一个名称为Test的类: 2.服务器端: 相对于客户端的实现,服务器端的实现稍复杂一点,因为前面讲过,一个服务器端可以接受N个客户端的连接,因此,在服务器端,有必要对每个连接上来的客户端进行登记,因此服务器端的程序结构包括了2个程序结构,第一个程序结构主要负责启动服务器、对来访的客户端进行登记和撤销,因此我们需要建立2个类。 第一个程序结构负责服务器的启动与客户端连接的登记,首先建立TcpListener网络侦听类,建立的时候构造函数分别包括localaddr和port2个参数,localaddr指的是本地地址,也就是服务器的IP地址,有人会问为什么它自己不去自动获得本机的地址?关于这个举个很简单的例子,服务器安装了2个网卡,也就有了2个IP地址,那建立服务器的时候就可以选择侦听的使用的是哪个网络端口了,不过一般的电脑只有一个网络端口,你可以懒点直接写个固定的函数直接获取IP地址System.Net.Dns.GetHostAddresses(System.Net.Dns.GetHostName())[0],GetHostAddresses函数就是获取本机的IP地址,默认选择第一个端口于是后面加个[0],第2个参数port是真侦听的端口,这个简单,自己决定,如果出现端口冲突,函数自己会提醒错误的。第二步,启动服务器,TcpListener.Start()。第三步,启动客户端的尝试连接,TcpListener.BeginAcceptTcpClient,入口2个参数,callback(客户端连接上后的返调函数),state(传递参数,跟第二节介绍的一样,随便什么都可以,建立把TcpListener自身传递过去),第四步,建立客户端连接上来后的返调函数,比如我们建立个名为void ClientAccept(IAsyncResult result)的函数,函数里,我们要获取客户端的对象,第三步里讲过我们传递TcpListener参数进去,在这里,我们通过入口参数获取它TcpListener tcplst = (TcpListener)result.AsyncState,获取客户端对象TcpClient bak_tcpclient = tcplst.EndAcceptTcpClient(result),这个bak_tcpclient我建议在类里面建立个列表,然后把它加进去,因为下一个客户端连接上来后此对象就会被冲刷掉了,客户端处理完毕后,接下来我们要启动下一个客户端的连接tcplst.BeginAcceptTcpClient(new AsyncCallback(sub_ClientAccept), tcplst),这个和第三步是一样的,我就不重复了。 第二个程序结构主要负责单个客户端与服务器端的处理程序,主要负责数据的通讯,方法很类似客户端的代码,基本大同,除了不需要启动连接的函数,因此这个程序结构主要启动下数据的侦听的功能、判断断开的功能、数据发送的功能即可,在第一个程序第四步我们获取了客户端的对象bak_tcpclient,在这里,我们首先启动数据侦听功能NetworkStream ns= bak_tcpclient.GetStream();ns.BeginRead(data, 0, 1024, new AsyncCallback(DataRec), data);这个跟我在第二节里介绍的是一模一样的(第二节第10行),还有数据的处理函数,数据发送函数,判断连接已断开的代码与第二节也是一模一样的,不过在这里我们需要额外的添加一段代码,当判断出连接已断开的时候,我们要将客户端告知第一个程序结构进行删除客户端操作,这个方法我的实现方法是在建立第二个程序结构的时候,将第一个程序结构当参数传递进来,判断连接断开后,调用第一个程序结构的公开方法去删除,即从客户端列表下删除此对象。 第一个程序结构我们定义一个TSever的类,第二个程序结构我们一个TClient的类,代码如下:TSever类
W5500是一种高度集成的以太网控制器,它集成了完整的TCP/IP协议,可以在FPGA中使用。W5500的FPGA源码是用于在FPGA芯片上实现W5500的功能的代码。 W5500的FPGA源码主要包括硬件描述语言(HDL)代码,如Verilog或VHDL,以及必要的配置和控制代码。FPGA源码的功能是在FPGA芯片上实现W5500的控制器,以便实现TCP/IP协议。 使用W5500的FPGA源码,我们可以借助FPGA的可编程性,将W5500的控制器功能集成到我们的FPGA设计中。这样,我们就可以实现一个基于W5500的以太网应用,如网络通信、远程控制等。 FPGA源码的编写需要具备一定的硬件描述语言知识,以及熟悉W5500芯片的功能和寄存器配置的技能。我们可以根据W5500的数据手册来编写FPGA源码,并根据需要进行相应的配置和控制。 使用W5500的FPGA源码可以带来一些优势。首先,FPGA的可编程性使得我们可以根据具体应用的需求进行自定义,实现更高性能和更好的定制性。其次,W5500的硬件卸载功能可以减轻主处理器的负荷,使其能够更专注于其他任务。此外,W5500的集成度较高,可以减少组件的复杂度和成本。 总之,W5500的FPGA源码是用于实现W5500控制器在FPGA芯片上的功能的代码。通过使用W5500的FPGA源码,我们可以实现一个基于W5500的高度集成的TCP/IP协议,并将其应用于各种网络通信和远程控制领域。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值