湘潭大学网络编程大题复习

本文详细介绍了网络编程中涉及的客户端和服务器库例程的实现,包括TCP和UDP协议的echo、daytime、time服务的客户和服务器程序。涵盖了socket创建、连接、读写、地址转换、错误处理等多个关键知识点,并提供了具体的代码示例。此外,还讨论了多线程和单线程并发服务器的实现。
摘要由CSDN通过智能技术生成

主要参考的博客:风落写的网络编程大题复习
学长的博客对复习帮助很大!

一、客户库例程实现

知识点

struct sockaddr

网络套接字地址结构体

typedef unsigned short sa_family_t;
struct sockaddr {
        sa_family_t     sa_family;    /* 地址族, AF_xxx       */
        char            sa_data[14];    /* 14字节的协议地址 */
}

struct addrinfo

struct addrinfo
{        
    int ai_flags;
    int ai_family;
    int ai_socktype;
    int ai_protocol;
    size_t ai_addrlen;
    struct sockaddr *ai_addr;/*核心就是想将IP地址和端口号转换成网络套接字地址结构体ai_addr*/
    char *ai_canonname;
    struct addrinfo *ai_next;
};

其中:

  • ai_flags:一些可选项。
  • ai_family:地址族,如AF_INET(ipv4)、AF_INET6(ipv6)。
  • ai_socktype:套接字类型,如 SOCK_STREAM(TCP), SOCK_DGRAM(UDP), SOCK_RAW。
  • ai_protocol:协议类型,如 IPPROTO_TCP、IPPROTO_UDP。
  • ai_addrlen:ai_addr的长度。
  • ai_addr:主机地址。
  • ai_canonname:主机名。
  • ai_next:指向下一个struct addrinfo的指针。

getaddrinfo()

将主机名转换为IP地址、服务名转换为端口号,转换成网络套接字地址结构体,并存储在result参数指向的结构体链表中。
IPv6中引入getaddrinfo(),它是协议无关的,既可用于 IPv4 也可用于IPv6 。

int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints,struct addrinfo **result )

参数说明:

  • hostname: 一个主机名或者地址串(IPv4 的点分十进制串或者 IPv6 的 16 进制串)。
  • service:服务名,可以是十进制的端口号,也可以是已定义的服务名称,如 ftp、http 等。
  • hints:一个指向 addrinfo 结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的选项。
  • result:本函数通过 result 指针参数返回一个指向 addrinfo 结构体链表的指针。

返回值:
0 成功,非0 出错。

客户端调用getaddrinfo()时,ai_flags一般不设置,主机名(一般为点分式的IP地址字符串)和服务名(一般为端口号)不为空。ai_protocol参数一般设置为0,可直接用IPPROTO_IP表示。

getaddrinfo()返回多个addrinfo结构的情形:

  • 如果与hostname参数关联的地址有多个,那么适用于请求地址簇的每个地址都返回一个对应的结构。
  • 如果service参数指定的服务支持多个套接字类型,那么每个套接字类型都可能返回一个对应的结构,具体取决于hints结构的ai_socktype成员。

由于getaddrinfo()可能返回多个地址,而result返回的只有一个结构体,因此需要遍历链表,逐个尝试每个返回地址。

freeaddrinfo()

void freeaddrinfo( struct addrinfo *ai );

参数ai应指向由getaddrinfo()返回的第一个addrinfo结构。函数功能是:这个链表中的所有结构以及它们指向的任何动态存储空间都被释放掉。

errx()

void errx(int eval, const char *fmt, ...)

在标准错误流中打印格式化的错误信息fmt,然后用参数eval调用exit(eval)。

strerror()

char *strerror(int errnum);

根据传入的错误号errnum,返回错误信息的字符串。

exit()

void exit(int status);

关闭所有文件,终止正在执行的进程。status是程序退出时的返回值,status为0表示正常退出,非0表示异常退出。

socket()

创建socket文件描述符。

int socket(int domain, int type, int protocol);

参数说明:

  • domain:地址族(IP 地址类型),如AF_INET(ipv4)、AF_INET6(ipv6)。
  • type:套接字类型(数据传输方式),如 SOCK_STREAM(TCP), SOCK_DGRAM(UDP)。
  • protocol:传输协议,如 IPPROTO_TCP(TCP 传输协议) 和 IPPTOTO_UDP(UDP 传输协议)。

返回值:
soket文件描述符,-1 创建失败。

connect()

建立与服务端的连接,成功返回0,否则-1。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

参数说明:

  • sockfd:socket文件描述符。
  • addr:服务端地址结构体。
  • addrlen:addr结构体的长度。

close()

int close(int sockfd);

把socket描述符 sockfd标记为关闭。
返回0 成功,-1失败。

代码

代码逻辑:

  • 调用getaddrinfo()函数,将主机名、服务名转换成addrinfo结构体。
  • 由于addrinfo结构体可能有多个,因此遍历该结构体链表,调用socket()创建socket套接字,再调用conncet()建立与服务器的连接,最后返回socket文件描述符。
/* connectsock.c - 使用TCP或者UDP分配并连接socket */
/* 此处省略预编译指令的#include 部分 */

int connectsock(const char *node, const char *service, const char *transport)
{
// *node主机名,*service服务名,*transport传输协议名,返回socket文件描述符

	struct addrinfo *result, *resptr; // result保存获取结果,resptr为结果链表指针
	struct addrinfo hints = { 0 };
	int ret, connectsocket, type; // ret记录错误号,connectsocket记录socket文件描述符
	if (strcmp(transport, "udp") == 0)
		type = ① SOCK_DGRAM; // type是套接字类型
	else if (strcmp(transport, "tcp") == 0)
		type = ② SOCK_STREAM;
	else
		errx(1, "connectsock arguments error: unknown transport\n");

	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = ③ type;
	hints.ai_protocol = 0;

	if ((ret = getaddrinfo ④ (node, service, &hints, &result)) != 0) // ret 不等于 0,表示失败
		errx(1, "getaddrinfo error: %s\n", gai_strerror(ret));
	for ⑤ (resptr = result; resptr != NULL; resptr = resptr->ai_next) {
		if ((connectsocket = socket ⑥ (resptr->ai_family, resptr->ai_socktype, resptr->ai_protocol)) < 0) {
			ret = errno; // socket 系统调用出错,则记录错误号
		}
		else if ((connect ⑦ (connectsocket, resptr->ai_addr, resptr->ai_addrlen)) < 0) {
			ret = errno; //connect调用出错,则记录错误号
			⑧ close(connectsocket); //关闭socket
		}
		else {
			freeaddrinfo(result); // 释放链表内存
			⑨ return connectsocket; // 返回socket文件描述符
		}
	}
	
	errno = ret;
	errx(1, "can't connect to %s.%s: %s\n", node, service, strerror(errno));
	exit(1);
}

二、利用daytime服务的TCP客户实现

知识点

main()的参数

int main (int argc,char *argv[])
  • argc参数:命令行中参数的个数,文件名本身也算一个参数。argc的值是在输入命令行时,由系统按实际参数的个数自动赋予的。
  • argv参数:字符串指针数组,各元素值为命令行中各字符串(参数均按字符串处理)的首地址。

read()

ssize_t read(int fd, void *buf, size_t nbytes);

函数功能:
read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf。成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

参数说明:

  • fd :要读取的socket文件描述符。
  • buf:要接收数据的缓冲区地址
  • nbytes:要读取的数据的字节数。

fputs()

int fputs(const char *str, FILE *stream);

把字符串str写入到指定的流 stream 中,但不包括空字符。
参数说明:

  • str:要写入的以空字符终止的字符序列。
  • stream:指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流。

代码

代码逻辑:

  • 调用connectsock()建立连接,得到socket文件描述符。
  • 建立连接后,客户端仅仅从该连接中读取输入read(),并将其打印fputs()。注意要用while循环读取,因为TCP发送数据会分片,只读取一次不能保证读取到了服务器发送过来的全部信息。
/* TCPdaytime.c - main, TCPdaytime */
/* 此处省略预编译指令的#include 部分 */

int TCPdaytime(int);
int connectsock(const char *, const char *, const char *);
#define LINELEN 4096  /* 文本行的最大长度 */
int main(int argc, char *argv[])
{
	char *host = "localhost"; /* 默认使用本机 */
	char *service = "daytime"; /* 默认服务端口 */
	int sock; /* socket文件描述符 */
	// 根据输入的参数个数,确定service和host
	switch (argc) {
	case 1:
		host = "localhost";
		break;
	case 3:
		service = argv[2];
	case 2:
		host = argv[1];
		break;
	default:
		errx(1, "usage: %s [host [port]]\n", ① argv[0]); // argv[0]是文件名:TCPdaytime
	}
	sock = connectsock ② (host, service, "tcp"); // 建立并连接套接字
	exit(③ TCPdaytime(sock));
}
int TCPdaytime(int sd)
{
	char buf[LINELEN+1]; /* 一行文本的缓冲区 */
	int nchars; /* 读取的字符数 */
	while ((nchars = ④ read(sd, buf, LINELEN))) {
		if ⑤ (nchars < 0)
			err(1, "TCPdaytime read error");
		buf[nchars] = ⑥ '\0'; /* 确保字符串结尾'\0' */
		fputs(buf, stdout);
	}
	return 0;
}

三、针对time服务的UDP客户实现

知识点

time_t

即long long。

write()

int write(int fd, const void *buf, unsigned int nbytes);

函数功能:将缓冲区 buf 中的 nbytes 个字节写入文件 fd。成功则返回写入的字节数,失败则返回 -1。
参数说明:

  • fd:要写入的文件的描述符
  • buf:要写入的数据的缓冲区地址
  • nbytes:要写入的数据的字节数。

ntohl()

随便理解:network to host long。
函数功能:将一个无符号长整形数网络字节序转换为主机字节序

asctime()

char *asctime(const struct tm *timeptr);

将时间 timeptr 转换为字符串。

localtime()

struct tm *localtime(const time_t *timer);

将表示日历时间的 time_t 值转换为struct tm结构。

代码

代码逻辑:

  • 客户端发送单个数据报的请求write(),服务器不处理这个数据报,只从中取出发送者的地址和协议端口号。
  • 服务器将当前时间编码为整数,用一个数据报发回给客户。因此客户端read()一次即可。
  • 客户端将收到的时间转换成本地主机字节序ntohl(),然后再转换为机器的本地表示localtime(),以字符串输出asctime()。
/* UDPtime.c - main, UDPtime */
/* 此处省略预编译指令的#include 部分 */

#define BUFFSIZE 8192 /* 缓冲区大小为读取和写入 */
#define UNIXEPOCH 2208988800UL /* UNIX epoch, in UCT secs */
#define MSG "what time is it?\n"
int UDPtime(int);
int connectsock(const char *, const char *, const char *);

int main(int argc, char *argv[])
{
	char *host = "localhost"; /* 默认本地 */
	char *service = "time"; /* 默认服务名称 */
	int sock; /* 套接字文件描述符 */
	switch (argc) {
	case 1:
		host = "localhost";
		break;
	case 3:
		service = argv[2];
	case 2:
		host = argv[1];
		break;
	default:
		errx(1, "usage: %s [host [port]]\n", ① argv[0]);
	}
	sock = connectsock ② (host, service, "udp");
	exit ③ (UDPtime(sock));
}
int UDPtime(int sd)
{
	time_t now; /* 使用 32 位整数来保存时间 */
	int nchars; /* 记录读取的字符数 */
	(void) write ④ (sd, MSG, strlen(MSG));
	nchars = read ⑤ (sd, (char *)&now, sizeof(now)); // 注意第二个参数要强制类型转换(char*)
	if ⑥ (nchar < 0)
		err(1, "UDPtime read error");
	now = ntohl ⑦ ((unsigned long)now); /* 转换为主机字节序 */
	now -= ⑧ UNIXEPOCH; /* UCT转换为UNIX计时 */
	printf("%s", asctime(localtime(&now)));
	return 0;
}

四、针对echo服务的客户实现

知识点

fgets()

char *fgets(char *str, int n, FILE *stream)

从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止。

代码

TCP

代码逻辑:

  • 从标准输入流中读取一行字符串到buf中。
  • 将buf中的字符串发送给服务器write()。
  • 通过循环读取服务器的回复read(),读到inchars个字节为止。
  • 读完后,用fputs()输出到stdout。
/* TCPecho.c - main, TCPecho */
/* 此处省略预编译指令的#include 部分 */

int TCPecho(int);
int connectsock(const char *, const char *, const char *);
#define LINELEN 128
/* main - ECHO 服务的 TCP 客户端 */
int main(int argc, char *argv[])
{
	char *host = "localhost";
	char *service = "echo";
	int socketd;
	switch (argc) {
	case 1:
		host = "localhost";
		break;
	case 3:
		service = argv[2];
	case 2:
		host = argv[1];
		break;
	default:
		errx(1, "usage: %s [host [port]]\n", ① argv[0]);
	}
	socketd = connectsock ② (host, service, "tcp");
	exit ③ (TCPecho(socketd));
}
/* TCPecho - 向服务端发送字符并打印服务端的答复 */
int TCPecho(int sock)
{
	char buf[LINELEN+1];
	int cc, inchars, outchars;
	while (fgets(buf, sizeof(buf), stdin)) {
		buf[LINELEN] = '\0';
		outchars = ④ strlen(buf);
		(void) write ⑤ (sock, buf, outchars);
		for ⑥ (inchars = 0; inchars < outchars; inchars += cc) {
			cc = read ⑦ (sock, &buf[inchars], outchars - inchars);
			if (cc < 0)
				err(1, "TCPecho read");
		}
		fputs(buf, stdout);
	}
	return 0;
}

UDP

代码逻辑:
和TCPecho的不同之处是,读取服务器的回复read()时,不需要用循环,直接读取一次即可。

/* UDPecho.c - main, UDPecho */
/* 此处省略预编译指令的#include 部分 */

int UDPecho(int);
int connectsock(const char *, const char *, const char *);
#define LINELEN 128
/* main - ECHO 服务的 UDP 客户端 */
int main(int argc, char *argv[])
{
	char *host = "localhost";
	char *service = "echo";
	int socketd;
	switch (argc) {
	case 1:
		host = "localhost";
		break;
	case 3:
		service = argv[2];
	case 2:
		host = argv[1];
		break;
	default:
		errx(1, "usage: %s [host [port]]\n", ① argv[0]);
	}
	socketd = connectsock ② (host, service, "udp");
	exit ③ (UDPecho(socketd));
}
/* UDPecho - 向服务端发送字符并打印服务端的答复 */
int UDPecho(int sock)
{
	char buf[LINELEN+1];
	int cc;
	while (fgets(buf, sizeof(buf), stdin)) {
		buf[LINELEN] = '\0';
		cc = ④ strlen(buf);
		(void) write ⑤ (sock, buf, cc);
		if (read ⑥ (sock, buf, cc) < 0)
			err(1, "UDPecho read");
		fputs(buf, stdout);
	}
	return 0;
}

五、服务器库例程实现

知识点

struct addrinfo

struct addrinfo
{        
    int ai_flags;
    int ai_family;
    int ai_socktype;
    int ai_protocol;
    size_t ai_addrlen;
    struct sockaddr *ai_addr;
    char *ai_canonname;
    struct addrinfo *ai_next;
};

补充参数:

  • ai_flags:
    • AI_PASSIVE
      • 如果getaddrinfo()的hostname是 NULL,则返回的socket描述符是通配符地址,可用于 bind() 函数,服务器可以使用这个通配符地址用来接收任何请求主机地址的连接。
      • 如果hostname不是NULL,那么 AI_PASSIVE 标志被忽略。
    • 还有一些其他的选项,但是不考。

通常服务器在调用getaddrinfo() 之前,ai_flags设置为AI_PASSIVE,让其地址支持bind()函数,绑定到套接字上。

setsockopt()

int setsockopt(int s, int level, int optname, const void * optval, ,socklen_toptlen);

函数功能:设置套接字s的可选项。
参数说明:

  • s:socket描述符
  • level:待设置的网络层, 此处设为IPPROTO_IPV6。
  • optname:待设置的选项,此处设为IPV6_V6ONLY,表示 AF_INET6 地址族创建的套接字是否仅适用于 IPv6 通信。如果非零,则套接字只能发送和接收 IPv6 数据包。;如果为零,套接字可用于向 IPv6 地址或 IPv4 地址发送数据包以及从 IPv6 地址或 IPv4 地址接收数据包。
  • optval:选项optname待设置的值。
  • socklen_toptlen:optval长度。

listen()

int listen(int sockfd, int backlog);

函数功能:
socket()函数创建一个套接字时,默认是一个主动套接字。listen()将socket文件描述符从主动转为被动,用于被动监听客户端的连接。

返回:
0 成功,-1 失败。

参数说明:

  • sockfd:socket文件描述符。
  • backlog:未完成连接队列+已完成连接队列的总长度(这个参数的解释比较玄乎,好像还有系统差异性,我随便理解理解)。

内核为任何一个监听套接字维护两个队列:

  • 未完成连接队列:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
  • 已完成连接队列:每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。

bind()

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数功能:
将socket文件描述符、端口号和ip绑定到一起,绑定的是本端信息。
参数说明:

  • sockfd:socket文件描述符。
  • addr:struct sockaddr结构的地址,用于设置要绑定的ip和端口。
  • addrlen:addr的大小。

为什么客户端不需要bind(),服务器端需要bind()呢?
浅理解一下,客户端的端口号是让操作系统随机分配的,而服务器端是,提供哪个服务,就必须用哪个熟知端口,所以需要手动给socket绑定IP和端口号。

代码

代码逻辑:

  • 调用getaddrinfo()返回struct addrinfo结构,里面是通配符地址。
  • 遍历struct addrinfo结构体链表,对每一个节点都首先创建socket(),然后setsockopt()稍微设置一下socket的参数,再给socket绑定bind()对应struct addrinfo结构中的IP和端口,接着调用listen()把主动改为被动监听版本的socket。
  • 一切完毕后,把动态内存清掉freeaddrinfo(),然后返回socket文件描述符。
/* passivesock.c - 使用TCP或者UDP分配并绑定一个server socket */
/* 此处省略预编译指令的#include 部分 */

int passivesock(const char *service, const char *transport, const int qlen)
{
// service服务名,transport协议名,qlen连接请求队列的长度(仅限于TCP)

	struct addrinfo *result, *resptr;
	struct addrinfo hints = { 0 };
	int sockopt = 0;
	int ret, passivesocket, type;
	if (strcmp(transport, "udp") == 0)
		type = ① SOCK_DGRAM;
	else if (strcmp(transport, "tcp") == 0)
		type = ② SOCK_STREAM;
	else
		errx(1, "passivesock arguments error: unknown transport\n");

	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = ③ type;
	hints.ai_flags = ④ AI_PASSIVE;
	hints.ai_protocol = 0;

	if ((ret = getaddrinfo ⑤ (NULL, service, &hints, &result)) != 0)
		errx(1, "getaddrinfo error: %s\n", gai_strerror(ret));
	for ⑥ (resptr = result; resptr != NULL; resptr = resptr->ai_next) {
		if ((passivesocket = socket ⑦ (resptr->ai_family, resptr->ai_socktype, resptr->ai_protocol)) < 0)
			ret = errno;
		else if (resptr->ai_family == AF_INET6 && setsockopt(passivesocket, IPPROTO_IPV6, IPV6_V6ONLY, &sockopt, sizeof(sockopt)) < 0) {
			ret = errno;
			⑧ close(passivesocket);
		}
		else if (bind ⑨ (passivesocket, resptr->ai_addr, resptr->ai_addrlen) < 0){
			ret = errno;
			⑩ close(passivesocket);
		}
		else if ⑪ (resptr->ai_socktype == SOCK_STREAM && listen(passivesocket, qlen) < 0){
			ret = errno;
			⑫ close(passivesocket);
		}
		else {
			freeaddrinfo(result);
			⑬ return passivesocket;
		}
	}
	errno = ret;
	errx(1, "can't allocate & bind %s socket: %s\n", service, strerror(errno));
	exit(1);
}

六、time服务器实现

知识点

struct sockaddr_storage

一个兼容IPV6的结构体。

struct sockaddr_storage
 {
     sa_family_t ss_family;      /* 协议族 */
     uint8_t       ss_len; // 表示该结构的长度
     char __ss_padding[_SS_PADSIZE];
};

好像用在了sendto和recvfrom里面???!!!

socklen_t

简单理解为int。

recvfrom()

int recvfrom(int s,char *buf,int len,int flags,sockaddr *from,int *fromlen);

函数功能:
接收数据报并存储源地址。

参数说明:

  • s:套接字描述符。
  • buf:接收数据的缓冲区。
  • len:缓冲区buf的长度。
  • flags:一组选项,用于修改函数调用的行为。
  • from:指向struct sockaddr结构的指针,将在recvfrom()函数返回时保存源地址。
  • fromlen:from的长度。

返回值:
如果未发生错误,返回收到的字节数。 如果连接已正常关闭,则返回值为零。 否则,将返回SOCKET_ERROR值。

time()

time_t time(time_t *timer)

参数说明:
timer=NULL时得到当前日历时间。

htonl()

Host to Network Long。
函数功能:
将主机的无符号长整形数转换成网络字节顺序。

sendto()

int sendto(int sockfd, const void *buf, size_t nsize, int flags, const struct sockaddr *to, const socklen_t addrlen);

函数功能:
将数据buf由指定的套接字sockfd 传给目的主机to。

参数说明:

  • sockfd:socket文件描述符。
  • buf:要发送的数据内容。
  • nsize:缓冲区buf的大小。
  • flags:一般为0。
  • to:目的地址,struct sockaddr结构。
  • addrlen:变量to的大小。

代码

代码逻辑:

  • 接收客户发来的数据报recvfrom()。由于服务器不需要查看客户端发送过来的数据,因此可以使用单个字符的缓存,如果数据报数据多于1个字节,则丢弃所有剩余字节。
  • 获取当前时间time(),转换为网络型htonl(),将时间返回给客户端sendto()。

UDP 是无连接协议,必须使用 sendto 函数发送数据,必须使用 recvfrom 函数接收数据,发送时需指明目的地址。sendto 函数与 send 功能基本相同, recvfrom 与 recv 功能基本相同,只不过 sendto 函数和 recvfrom 函数参数中都带有对方地址信息,这两个函数是专门为 UDP 协议提供的。

/* UDPtimed.c - main, UDPtimed */
/* 此处省略预编译指令的#include 部分 */

int passivesock(const char *, const char *, const int);
void UDPtimed(int);
# define UNIXEPOCH 2208988800UL /* UNIX epoch, in UCT secs */
int main(int argc, char *argv[])
{
	char *service = "time"; /* 默认服务名 */
	int sock; /* 服务器socket套接字 */
	switch ( argc ) {
	case 1:
		break;
	case 2:
		service = argv[ 1 ];
		break;
	default:
		errx(1, "usage: %s [port]\n", ① argv[0]);
	}
	sock = passivesock ② (service, "udp", 0);
	while (1) {
		UDPtimed ③ (sock);
	}
}
void UDPtimed(int sd)
{
	struct sockaddr_storage fsin; /* 一个客户端的请求地址 */
	char buf[1]; /* 服务端接收数据的缓冲区 */
	time_t now; /* 当前时间 */
	socklen_t alen; /* 请求地址的长度 */
	alen = sizeof(fsin);
	if (recvfrom ④ (sd, buf, sizeof(buf), 0, (struct sockaddr *)&fsin, &alen) < 0)
		err(1, "UDPtimed recvfrom error");
	(void) time(&now);
	now = htonl ⑤ ((unsigned long)(now + UNIXEPOCH));
	if (sendto ⑥ (sd, (char *)&now, sizeof(now), 0, (struct sockaddr *)&fsin, &alen) < 0)
		err(1, "UDPtimed sendto");
}

七、daytime服务器实现

知识点

accept()

阻塞型函数调用

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

函数功能:
accept()主要用在基于连接的套接字类型。它提取出监听套接字sockfd的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向新套接字的文件描述符。新套接字不在监听状态。

参数说明:

  • sockfd:监听套接字描述符。
  • addr:保存发起连接请求的主机的地址与端口的结构体变量,就是存放服务器接收请求的客户端的网络地址与端口的结构体变量。(这句话好绕-_-)
  • addrlen:addr长度。

代码

代码逻辑:

面向连接服务器

/* TCPdaytimed.c - main, TCPdaytimed */
/* 此处省略预编译指令的#include 部分 */

void TCPdaytimed(int);
int passivesock(const char *, const char *, const int);
#define QLEN 32 /* 连接队列的最大长度 */
int main(int argc, char *argv[])
{
	struct sockaddr_storage fsin; /* 客户端的请求地址 */
	char *service = "daytime"; /* 默认服务名 */
	int msock, ssock; /* 主、从服务器套接字 */
	socklen_t alen; /* 请求地址长度 */
	switch (argc) {
	case 1:
		break;
	case 2:
		service = argv[1];
		break;
	default:
		errx(1, "usage: %s [port]\n", ① argv[0]);
	}
	msock = passivesock ② (service, "tcp", QLEN);
	while (1) {
		alen = sizeof(fsin);
		ssock = accept ③ (msock, (struct sockaddr *)&fsin, &alen);
		if ④ (ssock < 0)
			err(1, "accept error");
		TCPdaytimed ⑤ (ssock);
		(void) close ⑥ (ssock);
	}
}
void TCPdaytimed(int sd)
{
	char *pts; /* 存时间的字符串 */
	time_t now; /* 当前时间 */
	(void) time(&now);
	pts = asctime ⑦ (localtime(&now)); 
	(void) write ⑧ (sd, pts, strlen(pts));
}

八、echo服务器实现

知识点

signal()

signal(SIGCHLD, reaper)

意思是:主服务器进程在收到子进程已退出的信号(SIGCHLD)后,就执行函数reaper。
reaper()函数会终止子进程并退出。

fork()

现有进程(父进程)调用fork()创建一个子进程。返回2次,子进程返回0,父进程返回子进程id。

代码

TCP

并发进程

/* TCPechod.c - main, TCPechod */
/* 此处省略预编译指令的#include 部分 */

#define QLEN 32
#define BUFSIZE 4096
void reaper(int);
int TCPechod(int);
int passivesock(const char *, const char *, const int);
/* main - 并发TCP服务器的ECHO服务 */
int main(int argc, char *argv[])
{
	char *service = "echo"; // 默认服务名
	struct sockaddr_storage fromsin; // 客户端地址
	socklen_t alen;
	int msocket, ssocket; // 主、从socket文件描述符
	switch (argc) {
	case 1:
		break;
	case 2:
		service = argv[1];
		break;
	default:
		errx(1, "usage: %s [port]\n", ① argv[0]);
	}
	msocket = passivesock ② (service, "tcp", QLEN);
	(void) signal(SIGCHLD, reaper);
	while (1) {
		alen = sizeof(fromsin);
		ssocket = accept ③ (msocket, (struct sockaddr *)&fromsin, &alen);
		if (ssocket < 0) {
			if (errno == EINTR)
				continue;
			err(1, "accept");
		}
		switch (fork()) {
		case 0: // 子进程
			(void) close ④ (msocket);
			exit ⑤ (TCPechod(ssocket));
		default: // 父进程
			(void) close ⑥ (ssocket);
			break;
		case -1:
			err(1, "fork");
		}
	}
}
/* TCPechod - 在给定的套接字上执行 TCP ECHO */
int TCPechod(int sock)
{
	char buf[BUFSIZE];
	int cc;
	while ((cc = read ⑦ (sock, buf, sizeof(buf)))) {
		if (cc < 0)
			err(1, "TCPechod read");
		if (write ⑧ (sock, buf, cc) < 0) // cc为 read 读取到的字符数
			err(1, "TCPechod write");
	}
	return 0;
}
/* reaper - clean up zombie children */
void reaper(int sig)
{
	int status;
	while (waitpid((pid_t)-1, &status, WNOHANG) >= 0);
}

UDP

迭代的、无连接算法

/* UDPechod.c - main */
/* 此处省略预编译指令的#include 部分 */

#define LINELEN 128
int UDPechod(int);
int passivesock(const char *, const char *, const int);
/* main - 迭代UDP服务器为ECHO服务 */
int main(int argc, char *argv[])
{
	char *service = "echo";
	int socketd;
	switch (argc) {
	case 1:
		break;
	case 2:
		service = argv[1];
		break;
	default:
		errx(1, "usage: %s [port]\n", ① argv[0]);
	}
	socketd = passivesock ② (service, "UDP", 0);
	while (1) {
		③ UDPechod(socketd);
	}
}
/* UDPechod - 在给定的套接字上执行 UDP ECHO */
int UDPechod(int sock)
{
	struct sockaddr_storage fromsin; // 客户端地址结构
	char buf[LINELEN+1];
	socklen_t alen;
	alen = sizeof(fromsin);
	if (recvfrom ④ (sock, buf, sizeof(buf), 0, (struct sockaddr *)&fromsin, &alen) < 0)
		err(1, "UDPechod recvfrom");
	(void) sendto ⑤ (sock, buf, strlen(buf), 0, (struct sockaddr *)&fromsin, alen);
	return 0;
}

九、多线程echo服务器实现

知识点

涉及到很多线程的知识。

pthread_t

unsigned long int。

pthread_attr_t

存储线程属性的结构体,结构太复杂了也不考。

pthread_attr_init()

初始化线程属性。
返回值:0 成功,非0 失败。

pthread_attr_setdetachstate()

设置线程的分离状态属性。
线程分离状态:线程以什么方式终止自己,包括PTHREAD_CREATE_DETACHED分离状态、PTHREAD_CREATE_JOINABLE正常状态。

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

返回:0 成功,非0 失败。

pthread_create()

创建线程。

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

返回值:
0 成功,非0 失败。

参数说明:

  • thread:新线程标识符。
  • attr:线程属性。
  • start_routine:新线程的线程函数开始地址。
  • arg:传递给start_routine函数的参数。

函数做函数的形参:
如 void *(*start_routine) (void ),按从左到右的顺序:
1 void
是start_routine函数的返回值类型
2 (*参数函数名)
3 start_routine函数的形参列表

pthread_mutex_init()

初始化互斥锁。

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

参数说明:

  • mutex:互斥锁结构。
  • attr:互斥锁属性。

返回值:
0 成功,其他 失败。

pthread_mutex_lock()

加锁。

int pthread_mutex_lock(pthread_mutex_t *mutex);

pthread_mutex_unlock()

解锁。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

代码

代码逻辑:
main():
初始化后,进入一个循环,调用accept()监听客户端的请求,每当有请求到达,则调用pthread_create()创建一个新线程来处理该连接,新线程去调用TCPechod过程。主线程继续循环。

TCPechod():
执行一个循环,重复从TCP连接读取数据,然后将数据反馈给发送者。

/* TCPmtechod.c - main, TCPechod, prstats */
/* 此处省略预编译指令的#include 部分 */

#define QLEN 32 /* 连接队列长度最大值 */
#define BUFSIZE 8192 /* 读写缓冲区最大值 */
#define INTERVAL 5 /* secs */
struct {
	pthread_mutex_t st_mutex; // 全局锁结构
	unsigned int st_concount; // 当前连接数
	unsigned int st_contotal; // 已完成连接数
	unsigned long st_contime; // 平均连接时长
	unsigned long st_bytecount; // 平均处理字节
} stats;
void prstats(void);
int TCPechod(int);
int passivesock(const char *, const char *, const int);
int main(int argc, char *argv[])
{
	pthread_t th;
	pthread_attr_t ta; // 线程属性
	int ret;
	char *service = "echo"; /* 服务名或者端口号 */
	struct sockaddr_storage fsin; /* 客户端请求地址 */
	socklen_t alen; /* 客户端地址长度 */
	int msock; /* 主套接字 */
	int ssock; /* 从套接字 */
	switch (argc) {
	case 1:
		break;
	case 2:
		service = argv[1];
		break;
	default:
		errx(1, "usage: %s [port]\n", ① argv[0]);
	}
	msock = passivesock ② (service, "tcp", QLEN);
	(void) pthread_attr_init ③ (&ta); // 初始化线程属性
	(void) pthread_attr_setdetachstate ④ (&ta, PTHREAD_CREATE_DETACHED); // 设置线程分离状态
	(void) pthread_mutex_init ⑤ (&stats.st_mutex, 0); // 初始化互斥锁
	if ((ret = pthread_create ⑥ (&th, &ta, (void * (*)(void *))prstats, 0)) < 0)
	errx(1, "pthread_create(prstats): %s\n", strerror(ret));
	while (1) {
		alen = sizeof(fsin);
		ssock = accept ⑦ (msock, (struct sockaddr *)&fsin, &alen);
		if ⑧ (ssock < 0) {
			if (errno == EINTR)
				continue;
			err(1, "accept error");
		}
		if ((ret = pthread_create ⑨ (&th, &ta, (void * (*)(void *))TCPechod, (void *)ssock)) < 0)
			errx(1, "pthread_create: %s\n", strerror(ret));
	}
}
int TCPechod(int sd)
{
	time_t start;
	char buf[BUFSIZE];
	int cc;
	start = time(0);
	(void) pthread_mutex_lock ⑩ (&stats.st_mutex);
	stats.st_concount++;
	(void) pthread_mutex_unlock ⑪ (&stats.st_mutex);
	while ((cc = read(sd, buf, sizeof buf))) {
		if (cc < 0)
			err(1, "TCPechod read error");
		if (write(sd, buf, cc) < 0)
			err(1, "TCPechod write error");
		(void) pthread_mutex_lock ⑫ (&stats.st_mutex);
		stats.st_bytecount += cc;
		(void) pthread_mutex_unlock ⑬ (&stats.st_mutex);
	}
	(void) close(sd);
	(void) pthread_mutex_lock ⑭ (&stats.st_mutex);
	stats.st_contime += time(0) - start;
	stats.st_concount--;
	stats.st_contotal++;
	(void) pthread_mutex_unlock ⑮ (&stats.st_mutex);
	return 0;
}
void prstats(void)
{
	time_t now;
	while (1) {
		(void) sleep(INTERVAL);
		(void) pthread_mutex_lock ⑯ (&stats.st_mutex);
		now = time(0);
		(void) printf("--- %s", ctime(&now));
		(void) printf("%-32s: %u\n", "Current connections", stats.st_concount);
		(void) printf("%-32s: %u\n", "Completed connections", stats.st_contotal);
		if (stats.st_contotal) {
			(void) printf("%-32s: %.2f (secs)\n",
			"Average complete connection time",
			(float)stats.st_contime /
			(float)stats.st_contotal);
			(void) printf("%-32s: %.2f\n",
			"Average byte count",
			(float)stats.st_bytecount /
			(float)(stats.st_contotal +
			stats.st_concount));
		}
		(void) printf("%-32s: %lu\n\n", "Total byte count", stats.st_bytecount);
		(void) pthread_mutex_unlock ⑰ (&stats.st_mutex);
	}
}

十、单线程并发echo服务器实现

知识点

fd_set类型

文件描述字的集合,用一位来表示一个文件描述符,有四个宏操作,如FD_ZERO()、FD_SET()、FD_CLR()、FD_ISSET()。

FD_ZERO()

int FD_ZERO(fd_set *fdset);

将指定的文件描述符集fdset清空,使fdset不包含任何文件句柄。

FD_SET()

int FD_SET(int fd, fd_set *fdset);

在文件描述符集合fdset中增加一个新的文件描述符fd。

FD_CLR()

int FD_CLR(int fd, fd_set *fdset);

在文件描述符集合fdset中删除一个文件描述符fd。

FD_ISSET()

int FD_ISSET(int fd,fd_set *fdset);

测试指定的文件描述符fd是否在该集合fdset中。

FD_SETSIZE

fd_set的描述字数量,集合中最多有多少个描述字。

memcpy()

void *memcpy(void *str1, const void *str2, size_t n);

从存储区 str2 复制 n 个字节到存储区 str1。

select()

select()是非阻塞方式,accept()是阻塞方式。

阻塞 vs 非阻塞
阻塞:进程执行阻塞函数,一定要等到事件的发生,否则就堵在那里。
非阻塞:进程执行非阻塞函数,不一定要等到事件的发生。若事件不发生,返回值不同,但进程可以继续执行。

int select(int nfds, fd_set *readset, fd_set *writeset,fd_set* exceptset, struct timeval *timeout);

函数功能:
测试指定的fd_set可读?可写?有异常条件待处理?

参数说明:

  • nfds:需要检查的文件描述字个数(即检查到fd_set的第几位),数值应该比三组fd_set中所含的最大fd值更大,一般设为三组fd_set中所含的最大fd值加1。
  • readset:检查可读性的一组文件描述字。
  • writeset:检查可写性的一组文件描述字。
  • exceptset:检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)。
  • timeout:超时时间。如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。有三种可能:
    • timeout=NULL(阻塞:select将一直被阻塞,直到某个文件描述符上发生了事件)。
    • timeout所指向的结构设为非零时间(等待固定时间:如果在指定的时间段里有事件发生或者时间耗尽,函数均返回)。
    • timeout所指向的结构,时间设为0(非阻塞:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生)。

返回值:
返回对应位仍然为1的fd的总数,出错则返回负值。

代码

代码逻辑:
单线程实现并发,其实就是用多进程。
main():

echo():

/* TCPmechod.c - main, echo */
/* 此处省略预编译指令的#include 部分 */

#define QLEN 32 /* 连接队列的最大长度 */
#define BUFSIZE 8192 /* 读写缓冲区大小 */
int passivesock(const char *, const char *, const int);
int echo(int);
int main(int argc, char *argv[])
{
	char *service = "echo"; /* 服务名称或端口号 */
	struct sockaddr_storage fsin; /* 客户端请求地址 */
	int msock; /* 主服务套接字 */
	fd_set rfds; /* 读取文件描述符集 */
	fd_set afds; /* 活动文件描述符集 */
	socklen_t alen; /* 请求地址长度 */
	int sd, nfds;
	switch (argc) {
	case 1:
		break;
	case 2:
		service = argv[1];
		break;
	default:
		errx(1, "usage: %s [port]\n", ① argv[0]);
	}
	msock = passivesock ② (service, "tcp", QLEN);
	nfds = FD_SETSIZE;
	FD_ZERO ③ (&afds);
	FD_SET ④ (msock, &afds);
	while (1) { // 等待一个或多个描述符准备就绪
		memcpy ⑤ (&rfds, &afds, sizeof(rfds));
		if (select ⑥ (nfds, &rfds, (fd_set *)0, (fd_set *)0, (struct timeval *)0) < 0)
			err(1, "select error");
		if (FD_ISSET ⑦ (msock, &rfds)) { // 主描述符准备就绪
			int ssock;
			alen = sizeof(fsin);
			ssock = accept ⑧ (msock, (struct sockaddr *)&fsin, &alen);
			if (ssock < 0)
				err(1, "accept error");
			FD_SET ⑨ (ssock, &afds);
		}
		for ⑩ (sd=0; sd<nfds; ++sd) // 从描述符准备就绪
			if (sd != msock && FD_ISSET ⑪ (sd, &rfds))
				if (echo(sd) == 0) {
					(void) close(sd);
					FD_CLR ⑫ (sd, &afds);
				}
	}
}
int echo(int sd)
{
	char buf[BUFSIZE];
	int cc;
	cc = read ⑬ (sd, buf, sizeof(buf));
	if (cc < 0)
		err(1, "echo read error");
	if (cc && write ⑭ (sd, buf, cc) < 0)
		err(1, "echo write error");
	return ⑮ cc;
}

十一、多协议daytime服务器实现

知识点

代码

代码逻辑:
一个线程,可以同时为TCP、UDP提供服务。
main():

  • 调用passivesock()创建2个主套接字tsock、usock,然后进入无限循环,在循环里用select()等待2个主套接字准备就绪。
  • 若tsock准备就绪,则用accept()建立一个新连接,并返回一个新套接字ssock,通过新套接字ssock把时间发给客户端,然后调用close()结束新连接。
  • 若usock准备就绪,则调用recvfrom()读取传入数据报,并调用sendto()将响应发回客户,仍然使用usock套接字。
/* daytimed.c - main, daytime */
/* 此处省略预编译指令的#include 部分 */

int daytime(char []);
int passivesock(const char *, const char *, const int);
#ifndef MAX
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#endif
#define QLEN 32
#define LINELEN 4096
int main(int argc, char *argv[])
{
	char *service = "daytime";
	char buf[LINELEN+1]; /* 一行文本的缓冲区 */
	struct sockaddr_storage fsin;
	socklen_t alen;
	int tsock; /* TCP 主套接字 */
	int usock; /* UDP 套接字 */
	int nfds;
	fd_set rfds; /* 可读取文件描述符 */
	switch (argc) {
	case 1:
		break;
	case 2:
		service = argv[1];
		break;
	default:
		errx(1, "usage: %s [port]\n", ① argv[0]);
	}
	
	tsock = passivesock ② (service, "tcp", QLEN);
	usock = passivesock ③ (service, "udp", 0);
	nfds = MAX(tsock, usock) + 1; /* fd 的最大位数 */
	FD_ZERO(&rfds);
	
	while (1) {
		FD_SET ④ (tsock, &rfds);
		FD_SET ⑤ (usock, &rfds);
		if (select ⑥ (nfds, &rfds, (fd_set *)0, (fd_set *)0, (struct timeval *)0) < 0)
			err(1, "select error");
		if (FD_ISSET ⑦ (tsock, &rfds)) { // 如果TCP主套接字准备就绪
			int ssock; /* TCP 从套接字 */
			alen = sizeof(fsin);
			ssock = accept ⑧ (tsock, (struct sockaddr *)&fsin, &alen);
			if ⑨ (ssock < 0)
				err(1, "accept error");
			daytime ⑩ (buf); // 当前日期填入缓冲区buf
			(void) write ⑪ (ssock, buf, strlen(buf));
			(void) close(ssock);
		}
		if (FD_ISSET ⑫ (usock, &rfds)) { // 如果UDP套接字准备就绪
			alen = sizeof(fsin);
			if (recvfrom ⑬ (usock, buf, sizeof(buf), 0, (struct sockaddr *)&fsin, &alen) < 0)
				err(1, "recvfrom error");
			daytime ⑭ (buf);
			(void) sendto ⑮ (usock, buf, strlen(buf), 0, (struct sockaddr *)&fsin, sizeof(fsin));
		}
	}
}
int daytime(char buf[])
{
	char *ctime();
	time_t now;
	(void) time(&now);
	sprintf(buf, "%s", asctime(localtime(&now)));
	return 0;
}

十二、多服务服务器实现

知识点

代码

代码逻辑:

  • 为svent中的每一个服务创建套接字,并初始化数据结构fd2sv数组,下标是套接字文件描述符,对应的内容是一个struct service结构。把为所有服务创建的主套接字都加入fd_set中。
  • 进入一个无限循环,每次都调用select(),等待套接字中的某一个准备就绪。当select返回时,服务器循环扫描每一个服务的套接字描述符,使用宏FD_ISSET()测试描述符是否就绪。若发现了就绪的描述符,就调用struct service中的对应函数来处理请求。
  • 若为TCP协议,先调用doTCP()过程,再间接调用sv_func()函数。若为UDP协议,直接调用sv_func()函数即可。
/* superd.c - main, doTCP, reaper */
/* 此处省略预编译指令的#include 部分 */

#define UDP_SERV 0
#define TCP_SERV 1
#define NOSOCK -1 /* 无效的套接字描述符 */
struct service {
	char *sv_name; // 服务名
	char sv_useTCP; // 是否使用TCP
	int sv_sock; // socket描述符
	void (*sv_func)(); // 服务函数入口
};
void TCPechod(int), TCPchargend(int), TCPdaytimed(int), TCPtimed(int);
int passivesock(const char *, const char *, const int);
void doTCP(struct service *);
static void reaper(int);
struct service svent[] =
{
	{ "echo", TCP_SERV, NOSOCK, TCPechod },
	{ "chargen", TCP_SERV, NOSOCK, TCPchargend },
	{ "daytime", TCP_SERV, NOSOCK, TCPdaytimed },
	{ "time", TCP_SERV, NOSOCK, TCPtimed },
	{ 0, 0, 0, 0 },
};
#ifndef MAX
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#endif /* MAX */
#define QLEN 32 /* 连接队列的最大长度 */
#define LINELEN 4096
extern unsigned short portbase;
int main(int argc, char *argv[])
{
	struct service *psv, /* 服务表指针 */
	*fd2sv[FD_SETSIZE]; /* socket文件描述符(下标)映射到服务struct service */
	int fd, nfds;
	fd_set afds, rfds; /* 可读取的文件描述符 */
	struct sigaction act;
	switch (argc) {
		case 1:
			break;
		case 2:
			portbase = (unsigned short) atoi(argv[1]);
			break;
		default:
			errx(1, "usage: %s [portbase]\n", ① argv[0]);
	}
	nfds = 0;
	FD_ZERO(&afds);
	for ② (psv=&svent[0]; psv->sv_name; ++psv) {
		if (psv->sv_useTCP)
			psv->sv_sock = passivesock ③ (psv->sv_name, "tcp", QLEN);
		else
			psv->sv_sock = passivesock ④ (psv->sv_name, "udp", 0);
		fd2sv[psv->sv_sock] = ⑤ psv;
		nfds = MAX(psv->sv_sock+1, nfds);
		FD_SET ⑥ (psv->sv_sock, &afds);
	}
	/* (void) signal(SIGCHLD, reaper); */
	/* The sigaction() function supersedes the signal() function,
	and should be used in preference. */
	act.sa_handler = reaper;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	act.sa_flags |= SA_RESTART;
	act.sa_flags |= SA_NODEFER;
	if (sigaction(SIGCHLD, &act, NULL) == -1)
		err(1, "sigaction error");
	while (1) {
		memcpy ⑦ (&rfds, &afds, sizeof(rfds));
		if (select ⑧ (nfds, &rfds, (fd_set *)0, (fd_set *)0, (struct timeval *)0) < 0) {
			if (errno == EINTR)
				continue;
			err(1, "select error");
		}
		for ⑨ (fd=0; fd<nfds; ++fd)
			if (FD_ISSET(fd, &rfds)) {
				psv = ⑩ fd2sv[fd];
				if (psv->sv_useTCP)
					doTCP ⑪ (psv);
				else
					psv->sv_func ⑫ (psv->sv_sock);
			}
	}
}
void doTCP(struct service *psv)
{
	struct sockaddr_storage fsin; /* the from address of a client */
	/* unsigned int alen; */
	socklen_t alen; /* from-address length */
	int fd, ssock;
	alen = sizeof(fsin);
	ssock = accept ⑬ (psv->sv_sock, (struct sockaddr *)&fsin, &alen);
	if ⑭ (ssock < 0)
		err(1, "doTCP accept error");
	switch (fork()) {
		case 0:
			break;
		case -1:
			err(1, "doTCP fork error");
		default:
			(void) close ⑮ (ssock);
		return; /* parent */
	}
	/* child */
	for ⑯ (fd = FD_SETSIZE; fd >= 0; --fd)
		if (fd != ssock)
			(void) close(fd);
	psv->sv_func ⑰ (ssock);
	exit(0);
}
static void reaper(int signum)
{
	int status;
	/* while (wait3(&status, WNOHANG, (struct rusage *)0) >= 0) */
	while (waitpid((pid_t)-1, &status, WNOHANG) >= 0)
	/* empty */;
}

/* sv_funcs.c - TCPechod, TCPchargend, TCPdaytimed, TCPtimed */
/* 此处省略预编译指令的#include 部分 */
#define BUFSIZE 8192 /* buffer size for reads and writes */
void TCPechod(int), TCPchargend(int), TCPdaytimed(int), TCPtimed(int);
void TCPechod(int sd)
{
	char buf[BUFSIZE];
	int cc;
	while ((cc = read ① (sd, buf, sizeof(buf)))) {
		if (cc < 0)
			err(1, "echo read error");
		if (write ② (sd, buf, cc) < 0)
			err(1, "echo write error");
	}
}
#define LINELEN 4096 /* max text line length */

void TCPchargend(int sd)
{
	char c, buf[LINELEN+2]; /* print LINELEN chars + \r\n */
	c = ' ';
	buf[LINELEN] = '\r';
	buf[LINELEN+1] = '\n';
	while (1) {
		int i;
		for (i=0; i<LINELEN; ++i) {
			buf[i] = c++;
			if (c > '~')
			c = ' ';
		}
		if (write(sd, buf, LINELEN+2) < 0)
			break;
	}
}
void TCPdaytimed(int sd)
{
	char buf[LINELEN], *ctime();
	time_t now;
	(void) time(&now);
	sprintf(buf, "%s", asctime(localtime(&now)));
	(void) write ③ (sd, buf, strlen(buf));
}
#define UNIXEPOCH 2208988800UL /* UNIX epoch, in UCT secs */
void TCPtimed(int sd)
{
	time_t now;
	(void) time(&now);
	now = htonl((unsigned long)(now + UNIXEPOCH));
	(void) write ④ (sd, (char *)&now, sizeof(now));
}

十三、使用echo服务器的并发客户实现

知识点

代码

/* TCPtecho.c - main, TCPtecho, reader, writer, mstime */
/* 此处省略预编译指令的#include 部分 */
int TCPtecho(fd_set *, int, int, int);
int reader(int, fd_set *);
int writer(int, fd_set *);
long mstime(unsigned long *);
int connectsock(const char *, const char *);
#ifndef MIN
#define MIN(x, y) ((x) < (y) ? (x) : (y))
#endif /* MIN */
#define BUFSIZE 8192 /* buffer size for reads and writes */
#define CCOUNT 64*1024 /* default character count */
char *hname[FD_SETSIZE]; /* fd to host name mapping */
int rc[FD_SETSIZE], wc[FD_SETSIZE]; /* read/write character counts */
char buf[BUFSIZE]; /* read/write data buffer */
int main(int argc, char *argv[])
{
	int ccount = CCOUNT;
	int i, hcount, maxfd, fd;
	int one = 1;
	fd_set afds;
	hcount = 0;
	maxfd = -1;
	for (i=1; i<argc; ++i) {
		if (strcmp(argv[i], "-c") == 0) {
			if (++i < argc && (ccount = atoi(argv[i])))
			continue;
			errx(1, "usage: %s [ -c count ] host1 host2...\n",
			argv[0]);
		}
		/* else, a host */
		fd = connectsock ① (argv[i], "echo", "tcp");
		
		if (ioctl(fd, FIONBIO, (char *)&one))
			err(1, "ioctl error");
			
		if (fd > maxfd)
			maxfd = fd;
			
		hname[fd] = argv[i];
		++hcount;
		FD_SET ② (fd, &afds);
	}
	exit(TCPtecho ③ (&afds, maxfd+1, ccount, hcount));
}
int TCPtecho(fd_set *pafds, int nfds, int ccount, int hcount)
{
	fd_set rfds, wfds; /* read/write fd sets */
	fd_set rcfds, wcfds; /* read/write fd sets 副本 */
	int fd, i;
	for (i=0; i<BUFSIZE; ++i) /* echo data */
		buf[i] = 'D';
		
	memcpy ④ (&rcfds, pafds, sizeof(rcfds));
	memcpy ⑤ (&wcfds, pafds, sizeof(wcfds));
	
	for ⑥ (fd=0; fd<nfds; ++fd)
		rc[fd] = wc[fd] = ccount;
		
	(void) mstime ⑦ ((unsigned long *)0); /* set the epoch */
	while (hcount) {
		memcpy ⑧ (&rfds, &rcfds, sizeof(rfds));
		memcpy ⑨ (&wfds, &wcfds, sizeof(wfds));
		if (select ⑩ (nfds, &rfds, &wfds, (fd_set *)0, (struct timeval *)0) < 0)
			err(1, "TCPtecho select error");
		for ⑪ (fd=0; fd<nfds; ++fd) {
			if (FD_ISSET ⑫ (fd, &rfds))
				if (reader ⑬ (fd, &rcfds) == 0)
					hcount--;
					
			if (FD_ISSET ⑭ (fd, &wfds))
				writer ⑮ (fd, &wcfds);
		}
	}
	return 0;
}
int reader(int fd, fd_set *pfdset)
{
	unsigned long now;
	int cc;
	cc = read(fd, buf, sizeof(buf));
	
	if (cc < 0)
		err(1, "reader read error");
		
	if (cc == 0)
		errx(1, "reader read: premature end of file\n");
		
	rc[fd] -= cc;
	
	if (rc[fd])
		return 1;
	(void) mstime(&now);
	printf("%s: %d ms\n", hname[fd], (int) now);
	(void) close(fd);
	FD_CLR(fd, pfdset);
	return 0;
}
int writer(int fd, fd_set *pfdset)
{
	int nchars;
	nchars = write(fd, buf, MIN((int)sizeof(buf), wc[fd]));
	
	if ( nchars < 0)
		err(1, "writer read error");
		
	wc[fd] -= nchars;
	if (wc[fd] == 0) {
		(void) shutdown(fd, 1);
		FD_CLR(fd, pfdset);
	}
	return 0;
}
long mstime(unsigned long *pms)
{
	/* static struct timeval epoch; */
	/* struct timeval now; */
	static struct timespec epoch;
	struct timespec now;
	/* if (gettimeofday(&now, (struct timezone *)0)) */
	if (clock_gettime(CLOCK_REALTIME, &now))
		err(1, "mstime clock_gettime error");
		
	if (!pms) {
		epoch = now;
		return 0;
	}
	*pms = (now.tv_sec - epoch.tv_sec) * 1000;
	*pms += (now.tv_nsec - epoch.tv_nsec + 500000)/ 1000000;
	return *pms;
}

考试感受:

老师仁慈,虽然大题划的范围是整个练习题,但是考的.c文件跟样卷是一样的,一个客户端例程库、服务器例程库、还有TCPecho、UDPecho、TCPechod、UDPechod。但是挖空跟样卷不太一样,挖空不超过练习题的范围。大题主要是掌握一些库函数的调用,比如read、write、connect、bind、recvfrom、sendto、pthread_create等,隐约记得这些都考了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值