日志标签 ‘网络编程’

Netlink编程-使用NETLINK_INET_DIAG协议

2012年8月25日

Netlink可以使得内核和用户进程进行双向通信,前文已经介绍过用户进程主动发起会话请求的例子。在那个示例程序中,必须同时编写用户态程序和内核模块,因为他们之间通信的协议是我们自己设定的,并没有使用netlink已有的通信协议。如果使用netlink已有的通信协议,那么我们无需编写内核模块,只需编写用户态程序即可。

本文将说明如何在用户态使用NETLINK_INET_DIAG协议。

1.创建netlink套接字

Netlink的使用方法与普通套接字并无太大差异,前文已经说明参数的差异,这里不再赘述。

struct sk_req {
	struct nlmsghdr nlh;
	struct inet_diag_req r;
};

int main(int argc, char **argv)
{
	int fd;
	struct sk_req req;
	struct sockaddr_nl dest_addr;
	struct msghdr msg;
	char buf[8192];
	char src_ip[20];
	char dest_ip[20];
	struct iovec iov;

	if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_INET_DIAG)) < 0) {
		eprint(__LINE__, errno, "socket");
		return -1;
	}

2.发送消息到内核

用户进程通过msghdr结构将消息发送到内核中,因此必须首先初始化msghdr类型的变量msg。该数据结构与iovec类型的变量iov和sockaddr_nl类型的变量dest_addr关联,iov指向数据缓冲区,dest_addr用于描述目的套接字地址。

这里需要将nlmsghdr结构中的nlmsg_type指定为TCPDIAG_GETSOCK,说明获取的是TCP套接字。同时需要将nlmsg_flags字段指定为NLM_F_REQUEST |  NLM_F_ROOT,NLM_F_REQUEST是所有向内核发出消息请求的用户进程所必须所设置的,NLM_F_ROOT则指明返回所有的套接字。

	req.nlh.nlmsg_len = sizeof(req);
	req.nlh.nlmsg_type = TCPDIAG_GETSOCK;
	req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ROOT;
	req.nlh.nlmsg_pid = 0;

	memset(&req.r, 0, sizeof(req.r));
	req.r.idiag_family = AF_INET;
	req.r.idiag_states = ((1 << TCP_CLOSING + 1) - 1);

	iov.iov_base = &req;
	iov.iov_len = sizeof(req);

	memset(&dest_addr, 0, sizeof(dest_addr));
	dest_addr.nl_family = AF_NETLINK;
	dest_addr.nl_pid = 0;
	dest_addr.nl_groups = 0;

	memset(&msg, 0, sizeof(msg));
	msg.msg_name = (void *)&dest_addr;
	msg.msg_namelen = sizeof(dest_addr);
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

数据缓冲区通过req结构来表示,它封装了两个数据结构nlmsghdr和inet_diag_req。前者用来表示netlink消息头,它是必须封装的数据结构。后者是NETLINK_INET_DIAG协议所特有的请求会话的数据结构,具体结构如下:

struct inet_diag_req {
        __u8    idiag_family;           /* Family of addresses. */
        __u8    idiag_src_len;
        __u8    idiag_dst_len;
        __u8    idiag_ext;              /* Query extended information */

        struct inet_diag_sockid id;

        __u32   idiag_states;           /* States to dump */
        __u32   idiag_dbs;              /* Tables to dump (NI) */
};

这里需要特别注意的是inet_diag_req结构中的idiag_states字段,它用来表示内核将要反馈哪些状态的套接字到用户空间。用户空间通过一个枚举类型来表示套接字状态:

enum
{
  TCP_ESTABLISHED = 1,
  TCP_SYN_SENT,
  TCP_SYN_RECV,
  TCP_FIN_WAIT1,
  TCP_FIN_WAIT2,
  TCP_TIME_WAIT,
  TCP_CLOSE,
  TCP_CLOSE_WAIT,
  TCP_LAST_ACK,
  TCP_LISTEN,
  TCP_CLOSING
};

idiag_states字段的每一位表示一个状态,因此通过位偏移可以将具体某个状态位置1。上述的实例程序中,将表示所有状态的位都置1,因此内核将向用户进程反馈所有状态的套接字。

	if (sendmsg(fd, &msg, 0) < 0) {
		eprint(__LINE__, errno, "sendmsg");
		return -1;
	}

初始化相关的数据结构之后,接下来用户进程通过sendmsg函数发送消息到内核中。

3.用户进程接收消息

用户进程通过两层循环来接受并处理内核发送的消息。外层循环通过recvmsg函数不断接收内核发送的数据,在接收数据之前还要将新的数据缓冲区buf与iov进行绑定。内层循环将内核通过一次系统调用所发送的数据进行分批处理。对于本文所描述的NETLINK_INET_DIAG协议,内核每次向用户进程发送的消息通过inet_diag_msg结构描述:

struct inet_diag_msg {
        __u8    idiag_family;
        __u8    idiag_state;
        __u8    idiag_timer;
        __u8    idiag_retrans;

        struct inet_diag_sockid id;

        __u32   idiag_expires;
        __u32   idiag_rqueue;
        __u32   idiag_wqueue;
        __u32   idiag_uid;
        __u32   idiag_inode;
};

每一次外层循环将接收到的数据存放在buf缓冲区中,该缓冲区中存放了多条消息,结构如下:

struct nlhdrmsg struct inet_idiag_msg || struct nlhdrmsg struct inet_idiag_msg || ……

按照这样的数据存储方式,内层循环要做的就是依次获取这些数据结构。由于每条数据报都至少封装了nlmsghdr结构,因此具体的处理方法通过NLMSG_XXX宏即可完成。

	memset(buf, 0 ,sizeof(buf));
	iov.iov_base = buf;
	iov.iov_len = sizeof(buf);

	while (1) {
		int status;
		struct nlmsghdr *h;

		msg = (struct msghdr) {
			(void *)&dest_addr, sizeof(dest_addr),
				&iov, 1, NULL, 0, 0
		};
		status = recvmsg(fd, &msg, 0);
		if (status < 0) {
			if (errno == EINTR)
				continue;
			eprint(__LINE__, errno, "recvmsg");
			continue;
		}

		if (status == 0) {
			printf("EOF on netlink\n");
			close(fd);
			return 0;
		}

		h = (struct nlmsghdr *)buf;

		while (NLMSG_OK(h, status)) {
			struct inet_diag_msg *pkg = NULL;

			if (h->nlmsg_type == NLMSG_DONE) {
				close(fd);
				printf("NLMSG_DONE\n");
				return 0;
			}

			if (h->nlmsg_type == NLMSG_ERROR) {
				struct nlmsgerr *err;
				err = (struct nlmsgerr*)NLMSG_DATA(h);
				fprintf(stderr, "%d Error %d:%s\n", __LINE__, -(err->error), strerror(-(err->error)));
				close(fd);
				printf("NLMSG_ERROR\n");
				return 0;
			}

			pkg = (struct inet_diag_msg *)NLMSG_DATA(h);
			print_skinfo(pkg);
			get_tcp_state(pkg->idiag_state);
			h = NLMSG_NEXT(h, status);
		}//while
	}//while
	close(fd);
	return 0;

NLMSG_OK宏每次判断buf中的数据是否读取完毕,NLMSG_DATA取当前netlink消息头结构紧邻的inet_diag_msg结构,NLMSG_NEXT则取下一个netlink消息头结构。

pkg指向当前获取到的消息,接下来具体需求处理各个字段即可。上述程序中,print_skinfo函数打印pkg中的各个字段,get_tcp_state则是打印每个套接字连接的状态。

Netlink编程-用户主动发起会话

2012年8月19日

Netlink是一种在内核态和用户态可以进行双向通信的机制,也就是说,用户进程既可以作为服务器端又可以作为客户端,内核也是如此。用户进程和内核谁是服务器端谁是客户端,这个问题与谁先主动发起数据交互会话有关。

用户进程主动向内核发起会话在Linux内核中很常见,比如系统调用、对/proc的操作等。本文通过详解一个简单的实例程序来说明用户进程通过netlink机制如何主动向内核发起会话。在该程序中,用户进程向内核发送一段字符串,内核接收到后再将该字符串后再重新发给用户进程。

用户态程序

netlink是一种特殊的套接字,在用户态除了一些参数的传递对其使用的方法与一般套接字无较大差异,。

1.宏与数据结构的定义

在使用netlink进行用户进程和内核的数据交互时,最重要的是定义好通信协议。协议一词直白的说就是用户进程和内核应该以什么样的形式发送数据,以什么样的形式接收数据。而这个“形式”通常对应程序中的一个特定数据结构。

本文所演示的程序并没有使用netlink已有的通信协议,因此我们自定义一种协议类型NETLINK_TEST。

#define NETLINK_TEST 18
#define MAX_PAYLOAD 1024

struct req {
	struct nlmsghdr nlh;
	char buf[MAX_PAYLOAD];
};

除此之外,我们应该再自定义一个数据报类型req,该结构包含了netlink数据包头结构的变量nlh和一个MAX_PAYLOAD大小的缓冲区。这里我们为了演示简单,并没有像上文中描述的那样将一个特定数据结构与nlmsghdr封装起来。

2.创建netlink套接字

要使用netlink,必须先创建一个netlink套接字。创建方法同样采用socket(),只是这里需要注意传递的参数:

	int sock_fd;
	sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
	if (sock_fd < 0) {
		eprint(errno, "socket", __LINE__);
		return errno;
	}

第一个参数必须指定为PF_NETLINK或AF_NETLINK。第二个参数必须指定为SOCK_RAW或SOCK_DGRAM,因为netlink提供的是一种无连接的数据报服务。第三个参数则指定具体的协议类型,我们这里使用自定义的协议类型NETLINK_TEST。

另外,eprint()是一个自定义的出错处理函数,实现如下:

void eprint(int err_no, char *str, int line)
{
	printf("Error %d in line %d:%s() with %s\n", err_no, line, str, strerror(errno));
}

3.将本地套接字与源地址绑定

将本地的套接字与源地址进行绑定通过bind()完成。在绑定之前,需要将源地址进行初始化,nl_pid字段指明发送消息一方的pid,nl_groups表示多播组的掩码,这里我们并没有涉及多播,因此默认为0。

	struct sockaddr_nl src_addr;
	memset(&src_addr, 0, sizeof(src_addr));
	src_addr.nl_family = AF_NETLINK;
	src_addr.nl_pid = getpid();
	src_addr.nl_groups = 0;

	if (bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) < 0) {
		eprint(errno, "bind", __LINE__);
		return errno;
	}

4.初始化msghdr结构

用户进程最终发送的是msghdr结构的消息,因此必须对这个结构进行初始化。而此结构又与sockaddr_nl,iovec和nlmsghdr三个结构相关,因此必须依次对这些数据结构进行初始化。

首先初始化目的套接字的地址结构,该结构与源套接字地址结构初始化的方法稍有不同,即nl_pid必须为0,表示接收方为内核。

	struct sockaddr_nl dest_addr;
	memset(&dest_addr, 0, sizeof(dest_addr));
	dest_addr.nl_family = AF_NETLINK;
	dest_addr.nl_pid = 0;
	dest_addr.nl_groups = 0;

接下来对req类型的数据报进行初始化,即依次对其封装的两个数据结构初始化:

	struct req r;
	r.nlh.nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
	r.nlh.nlmsg_pid = getpid();
	r.nlh.nlmsg_flags = 0;
	memset(r.buf, 0, MAX_PAYLOAD);
	strcpy(NLMSG_DATA(&(r.nlh)), "hello, I am edsionte!");

这里的nlmsg_len为为sizeof(struct nlmsghdr)+MAX_PAYLOAD的总和。宏NLMSG_SPACE会自动将两者的长度相加。接下来对缓冲区向量iov进行初始化,让iov_base字段指向数据报结构,而iov_len为数据报长度。

	struct iovec iov;
	iov.iov_base = (void *)&r;
	iov.iov_len = sizeof(r);

一切就绪后,将目的套接字地址与当前要发送的消息msg绑定,即将目的套接字地址复制给msg_name。再将要发送的数据iov与msg_iov绑定,如果一次性要发送多个数据包,则创建一个iovec类型的数组。

	struct msghdr msg;
	msg.msg_name = (void *)&dest_addr;
	msg.msg_namelen = sizeof(dest_addr);
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

5.向内核发送消息

发送消息则很简单,通过sendmsg函数即可完成,前提是正确的创建netlink套接字和要发送的消息。

	if (sendmsg(sock_fd, &msg, 0) < 0) {
		eprint(errno, "sendmsg", __LINE__);
		return errno;
	}

6.接受内核发来的消息

如果用户进程需要接收内核发送的消息,则需要通过recvmsg完成,只不过在接收之前需要将数据报r重新初始化,因为发送和接收时传递的数据结构可能是不同的。

为了简单演示netlink的用法,本文所述的用户进程发送的是一段字符串,这一点从数据报结构req的定义可以看出。而内核向用户进程发送的也是一段字符串,具体情况下面将会具体说明。

        memset(&r.nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
        if (recvmsg(sock_fd, &msg, 0) < 0) {
                eprint(errno, "recvmsg", __LINE__);
                return errno;
        }

        printf("Received message payload:%s\n", (char *)NLMSG_DATA(&r.nlh));
        close(sock_fd);

接收完毕后,通过专门的宏NLMSG_DATA对数据报进行操作。 netlink对数据报的的访问和操作都是通过一系列标准的宏NLMSG_XXX来完成的,具体的说明可以通过man netlink查看。这里的NLMSG_DATA传递进去的是nlh,但它获取的是紧邻nlh的真正数据。本程序中传递的是字符串,所以取数据时候用char *强制类型转换,如果传递的是其他数据结构,则相应转换数据类型即可。

内核模块

netlink既然是一种用户态和内核态之间的双向通信机制,那么除了编写用户程序还要编写内核模块,也就是说用户进程和内核模块之间对数据的处理要彼此对应起来。

1.内核模块加载和卸载函数

内核模块加载函数主要通过netlink_kernel_create函数申请服务器端的套接字nl_sk,内核中对套接字表示为sock结构。另外,在创建套接字时还需要传递和用户进程相同的netlink协议类型NETLINK_TEST。创建套接字函数的第一个参数默认为init_net,第三个参数为多播时使用,我们这里不使用多播因此默认值为0。nl_data_handler是一个钩子函数,每当内核接收到一个消息时,这个钩子函数就被回调对用户数据进行处理。

#define NETLINK_TEST 17
struct sock *nl_sk = NULL;
static int __init hello_init(void)
{
	printk("hello_init is starting..\n");
	nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 0, nl_data_ready, NULL, THIS_MODULE);
	if (nl_sk == 0)
	{
		printk("can not create netlink socket.\n");
		return -1;
	}
	return 0;
}

内核模块卸载函数所做的工作与加载函数相反,通过sock_release函数释放一开始申请的套接字。

static void __exit hello_exit(void)
{
	sock_release(nl_sk->sk_socket);
	printk("hello_exit is leaving..\n");
}

2.钩子函数的实现

在内核创建netlink套接字时,必须绑定一个钩子函数,该钩子函数原型为:

 void (*input)(struct sk_buff *skb);

钩子函数的实现主要是先接收用户进程发送的消息,接收以后内核再发送一条消息到用户进程。

在钩子函数中,先通过skb_get函数对套接字缓冲区增加一次引用值,再通过nlmsg_hdr函数获取netlink消息头指针nlh。接着使用NLMSG_DATA宏获取用户进程发送过来的数据str。除此之外,再打印发送者的pid。

void nl_data_handler(struct sk_buff *__skb)
{
	struct sk_buff *skb;
	struct nlmsghdr *nlh;
	u32 pid;
	int rc;
	char str[100];
	int len = NLMSG_SPACE(MAX_PAYLOAD);

	printk("read data..\n");
	skb = skb_get(__skb);

	if (skb->len >= NLMSG_SPACE(0)) {
		nlh = nlmsg_hdr(skb);
		printk("Recv: %s\n", (char *)NLMSG_DATA(nlh));
		memcpy(str, NLMSG_DATA(nlh), sizeof(str));
		pid = nlh->nlmsg_pid;
		printk("pid is %d\n", pid);
		kfree_skb(skb);

接下来重新申请一个套接字缓冲区,为内核发送消息到用户进程做准备,nlmsg_put函数将填充netlink数据报头。接下来将用户进程发送的字符串复制到nlh紧邻的数据缓冲区中,等待内核发送。netlink_unicast函数将以非阻塞的方式发送数据包到用户进程,pid具体指明了接收消息的进程。

		skb = alloc_skb(len, GFP_ATOMIC);
		if (!skb){
			printk(KERN_ERR "net_link: allocate failed.\n");
			return;
		}
		nlh = nlmsg_put(skb, 0, 0, 0, MAX_PAYLOAD, 0);
		NETLINK_CB(skb).pid = 0;

		memcpy(NLMSG_DATA(nlh), str, sizeof(str));
		printk("net_link: going to send.\n");
		rc = netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
		if (rc < 0) {
			printk(KERN_ERR "net_link: can not unicast skb (%d)\n", rc);
		}
		printk("net_link: send is ok.\n");
	}
}

这样就完成了内核模块的编写,它与用户进程通信共同完成数据交互。

Netlink编程-数据结构

2012年8月18日

内核态与用户态进行数据交互的方法很多,分为用户程序主动发起的消息交交互和内核主动发起的信息交互。通常我们所熟知的系统调用、对/proc进行读写操作、mmap和编写驱动程序等都属于前者,而由内核主动发起的信息交互方法比较少,最典型的方法为内核发送信号给当前进程。上述这些方法都是单向通信的,也就是说要么是用户态进程主动发起数据交互会话,要么是内核主动发起会话。有没有一种可以在用户态和内核态进行双向数据交互的方法呢?

Netlink是一种在内核态和用户态可以进行双向数据交互的通信机制。在用户态,我们可以将netlink看作是一种特殊的socket,因此通过socket接口来就可以直接使用netlink;在内核态,则需要通过一组特殊的内核接口编写内核模块。

如果初次编写netlink程序,可能会对它所涉及的数据结构感到困惑,本文将简单介绍一下netlink编程中遇到的基本数据结构。

struct msghdr

如果使用sendmsg()发送数据,那么必须使用msghdr结构,该结构内部封装了本次发送数据的一些参数:

           struct msghdr {
               void         *msg_name;       /* optional address */
               socklen_t     msg_namelen;    /* size of address */
               struct iovec *msg_iov;        /* scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* ancillary data, see below */
               size_t        msg_controllen; /* ancillary data buffer len */
               int           msg_flags;      /* flags on received message */
           };

netlink提供的是一种基于数据报的通信服务,因此与UDP通信协议类似,必须通过msg_name指明目的套接字地址结构,而msg_namelen则指明该地址结构的长度。msg_iov指定数据缓冲区数组,而msg_iovlen指明了该数组的元素个数。

struct iovec

iovec结构表示一个向量元素,它定义了一个标准的数据缓冲区格式。其包含两个字段:指向数据的指针和数据长度。

struct iovec
{
    void *iov_base;     /* Pointer to data.  */
    size_t iov_len;     /* Length of data.  */
};

当使用iovec结构类型的数组传递数据时,可以将多个消息通过一次系统调用进行发送。

struct nlmsghdr

如果使用sendmsg函数来发送netlink数据报,那么iovec结构中iov_base字段应该根据具体协议指向一个自定义的数据结构。一般这个自定义的数据结构如下所示:

struct req {
        struct nlmsghdr *nlh;
        struct special_struct *r;
};

其中nlmsghdr结构为必须所有的,它用来描述netlink消息头。

           struct nlmsghdr {
               __u32 nlmsg_len;    /* Length of message including header. */
               __u16 nlmsg_type;   /* Type of message content. */
               __u16 nlmsg_flags;  /* Additional flags. */
               __u32 nlmsg_seq;    /* Sequence number. */
               __u32 nlmsg_pid;    /* PID of the sending process. */
           };

对于special_struct结构则要根据具体需求而定。如果使用netlink现有的协议,比如NETLINK_INET_DIAG,那么special_struct结构在用户程序向内核发送消息时应该为inet_diag_req结构,如果当内核发送消息给用户进程那么special_struct结构又必须定义为inet_diag_msg。这些结构体都是事先定义好的,我们直接拿来用,按需获取这些结构中的某些字段的值即可。

如果是自己定义的通信协议,那么则根据具体需求自定义数据结构即可。比如用户进程想通过pid在内核中获取该进程亲属的pid,那么用户进程在发送消息和接受消息时的special_struct分别可以按照如下定义:

struct get_pid_req {
        int pid;
};

struct get_pid_msg {
        int parent_pid;
        int sibling_pid;
        int child_pid;
}

为netlink消息确定好了结构以后,则将该结构赋值给iovec结构的iov_base字段。

struct sockaddr_nl

netlink是一种特殊的无连接套接字,因此必须在msghdr结构的msg_name中指明目的netlink套接字的地址。套接字地址的通用结构为struct sockaddr,但对于具体类型的套接字则有不同的地址结构,比如TCP/IP协议族为sockaddr_in,对于netlink则为sockaddr_nl结构:

           struct sockaddr_nl {
               sa_family_t     nl_family;  /* AF_NETLINK */
               unsigned short  nl_pad;     /* Zero. */
               pid_t           nl_pid;     /* Process ID. */
               __u32           nl_groups;  /* Multicast groups mask. */
           };

其中该结构中的nl_family字段填充为AF_NETLINK。

以上这些结构在netlink编程中都会涉及到,但是通过上面描述的关系来看,它们最终都是与msghdr结构有关的。而msghdr结构作为参数通过sendmsg函数就可以netlink数据包发送至内核。

TCP连接的建立和终止

2011年6月9日

在基于TCP的客户/服务器模型中,服务器和客户程序之间通过一系列的socket接口形成TCP连接。服务器启动后的首要工作就是创建监听套接字,监听套接字是通过socket、bind和listen三个函数完成的。在这之后,监听套接字通过accept函数一直处于阻塞状态,等待客户程序的连接请求。客户程序在连接至服务器之前,必须先创建自己的套接字,再通过connect函数连接到服务器。如果一切顺利,那么服务器和客户之间就形成了一条TCP连接。

上述TCP连接的过程是从编程角度触发,接下来我们将从网络通信的角度分析TCP的建立和终止过程,并且结合相关的socket函数接口分析其建立和终止过程中客户程序和服务器状态的变化。

1.TCP的建立

TCP连接建立过程即通常我们所说的“三路握手”。整个连接过程是由客户端的connect函数发起的,其具体的步骤如下:

1.客户端向服务器发送一个SYN(同步)报文段,这个报文段包含了客户端将要发送数据的初始序列号,假设该序号为J。该SYN段为整个TCP连接过程的报文段1。

2.服务器向客户端发送一个SYN段作为对客户端的应答,该报文段包含服务器在此连接中将要发送数据的起始序列号K。该SYN段为整个TCP连接过程的报文段2;在该报文段中还包含服务器对客户端SYN段的确认(ACK),该确认包含的序号为J+1,表明服务器期望客户端下次发送的数据序号是从J+1开始的。也就是说报文段2包含服务器发送的SYN和服务器对客户SYN的ACK。

3.客户端向服务器发送ACK以对服务器SYN进行确认,该报文段的序号为K+1。这是报文段3。

由于服务器在接收到客户端的TCP连接请求之前调用了accpet函数,所以一直处于阻塞状态。当客户通过connect函数向服务器发送第一个SYN段时,它执行了主动打开TCP连接这个动作。而接受这个SYN的服务器则执行被动打开动作,它向客户发送SYN以及ACK确认。

接下来我们以上文所举例的回射客户/服务器程序为例,说明TCP连接过程中客户端和服务器的状态变化。

1.从后台启动服务器程序。通过netstat命令我们可以看到当前服务器处于监听状态,因此此刻还没有任何客户请求。

在本地地址一栏中,IP地址为*是因为我们在绑定(bind)服务器套接字时使用了INADDR_ANY参数。由于此刻没有任何客户请求,所以外来地址处均为*。

2。运行客户程序,TCP连接建立。第一条信息为主服务器进程,它此刻仍然处于监听状态,等待其他客户程序的连接请求;第二条信息为子服务器进程,它此刻已经建立了TCP连接,本地端口为6666,而外来连接端口为54260。第三条信息为客户客户进程,他此刻也处于已建立连接的状态,本地端口为54260,而外来端口正好为6666。

此刻,我们在客户端并没有输入任何数据,因此此刻主服务器进程、子服务器进程和客户进程均处于阻塞状态,但是其阻塞原因却不同。主服务器进程是因为等待(accept)其他客户请求而阻塞;子服务器进程是因为等待(read())客户进程发送的数据而阻塞;客户进程则是因为等待从标准输入读取(fgets())数据而阻塞。我们可以进一步使用ps命令查看三个进程之间的状态。如下:

对ps使用-t选项可以查看指定伪终端下的进程,而通过-o选项则可以输入指定的进程状态信息,WCHAN参数可以进程睡眠的原因。ps的输出结果和我们上面的分析的结果一致,三个进程确实处于睡眠状态(S)。当进程阻塞于accept或connect时,其睡眠条件为inet_csk_wait_for_connect;当进程阻塞于套接字输入或输出时,其睡眠条件为sk_wait_data;当进程阻塞于终端的读操作时,其睡眠条件为n_tty_read。

2.TCP的终止

由于TCP连接是全双工的,因此必须分别关闭两个方向上的连接。通常一个TCP连接的终止需要4个分节。整个TCP的终止过程通常是由客户端的close函数发起的。

1.客户端进程调用close函数关闭套接字,它执行了主动关闭TCP连接,该端TCP向服务器发送一个FIN报文段(报文段1),表示该端的数据已经发送完毕。假设该报文段的序号为M。

2.服务器接收到这个FIN段后执行被动关闭。为了确认受到这个FIN,服务器向客户端发送一个ACK(报文段2),该确认报文的序号为M+1。服务器接收了客户端发送来的FIN说明服务器在该TCP连接上再无数据可接收。

3.服务器程序调用close关闭套接字,这将导致服务器向客户端发送一个FIN报文(报文段3),假设该报文的序号为N。

4.客户端为了确认服务器发送来的FIN报文,它向服务器发送一个ACK(报文4),该报文的序号为N+1。

上述所提及的关闭套接字有时候并不是只能通过close函数来实现。客户进程不论是正常终止(通过exit或者从主函数中返回)还是通过某个信号而终止时,都会关闭所有已经打开的描述符,这里当然包括套接字描述符。

当用户在标准输入端输入EOF时(Contrl+D),客户正常退出而关闭套接字,紧接着就会发生上述的TCP终止过程。通过netstat命令可以看到当前TCP的连接状态。

当前连接的客户端进入了TIME_WAIT状态,而服务器仍然在等待其他客户的连接。

我们继续使用ps可以查看到当前客户和服务器进程的状态。

可以看到客户进程已经退出,主服务器进程由于等待其他客户的连接而处于睡眠状态,子服务器进程虽然已经停止了服务,但是仍然处于“阴魂不散”的僵死状态。这是由于主服务器进程并未对子服务器进程发出的SIGCHLD信号进行捕获,具体的捕获方法在这里的文章已经说明。

TCP并发服务器程序设计

2011年6月8日

TCP客户/服务器程序设计模型-并发服务器

上文中所说的迭代服务器简单易懂,将它作为学习socket编程的例子再适合不过了。但是在一般的实际应用中我们并不会使用这个简单的迭代服务器,因为这样的服务器每次只能服务一个客户,如果这个客户请求长时间占用服务器,那么其他客户将一直不能被服务。较为理想的情况是服务器可以同时为多个客户服务,接下来将要说明的并发服务器就可以满足这样的要求。

1.并发服务器原理

关于并发服务器的基本原理可以参考本博客之前的文章,这里不在赘述。下面将通过一个具有并发服务器的C/S模型示例来说明并发服务器程序设计的细节。

2客户程序设计

客户程序与上文所述的客户程序类似,只不过在客户端进行connect之后,客户端与服务器之间的所有数据交互都封装在了str_cli函数中。客户程序主函数在调用该函数时不仅要向该函数传递套接字,还要传递标准输入文件指针stdin。具体代码如下:

void str_cli(FILE *fp, int sockfd)
{
	char sendline[DATA_MAX], recvline[DATA_MAX];

	memset(sendline, 0, DATA_MAX);
	memset(recvline, 0, DATA_MAX);

	while(fgets(sendline, DATA_MAX, fp) != NULL) {
		if (write(sockfd, sendline, strlen(sendline)) < 0) {
			printf("write error!\n");
			exit(1);
		}

		if (read(sockfd, recvline, DATA_MAX) == 0) {
			printf("read error!\n");
			exit(1);
		}
		fputs(recvline, stdout);

          	memset(sendline, 0, DATA_MAX);
         	memset(recvline, 0, DATA_MAX);
	}
}

从客户端的数据交互函数str_cli中可以看出客户端进行的操作如下:

1.客户程序通过fgets从标准输入文件读入数据,并通过write将数据发送至服务器端;

2.客户程序通过read函数读取服务器发来的数据,并通过fputs将数据写入标准输出文件;

3.如果fgets读入数据错误时,整个循环结束,客户端也将结束;

3.服务器程序设计

并发服务器的经典模型在本博客之前的文章中有所提到,这里只是特别说明服务器数据处理函数。

void str_echo(int sockfd)
{
	int n;
	char buf[DATA_MAX];

again:
	while((n = read(sockfd, buf, DATA_MAX)) > 0)
		if (write(sockfd, buf, n) < 0) {
			printf("write error!\n");
			exit(1);
		}

	if (n < 0 && errno == EINTR)
		goto again;
	else if (n < 0)
		printf("str_echo error!\n");
}

服务器程序完成的工作如下:

1.通过read读取客户程序发送的数据;

2.通过write将刚刚读到的数据重新发送给客户程序;

3.如果客户端正常退出,那么服务器的read函数将返回0并退出循环;否则将显示错误信息;

通过上述对客户和服务器程序的描述,我们可以得知该C/S模型的工作方式是:客户程序从标准输入读入用户输入的数据,并发送至服务器;服务器从网络输入读入该数据,立即将读入的数据发送给客户程序;客户从网络输入读取服务器发来的数据,并将其显示在标准输出上。

也就是说,每当用户在客户端输入数据,客户端在终端上又会立即回显用户所输入的数据。因此,我们将上述所举例的具有并发服务器的C/S模型称之为回射客户/服务器程序。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key