60.1 迭代型和并发型服务器 1016
1.迭代型: 服务器每次只处理一个客户端,只有当完全处理完一个客户端的请求后才会去处理下一个客户端。只适用于快速处理客户端请求的场景,因为每个客户端都必须等待,直到前面所有的客户端都处理完了服务器才能继续服务下一个客户端。
2.并发型: 服务器能够同时处理多个客户端的请求。适用于每个请求都需要大量处理时间,或是当客户端和服务器在进行扩展对话中需要来回传递信息的场景。本章重点
60.2 迭代型UDP echo服务器 1016
#include <syslog.h>
#include "id_echo.h"
#include "become_daemon.h"
int
main(int argc, char *argv[])
{
int sfd;
ssize_t numRead;
socklen_t len;
struct sockaddr_storage claddr;
char buf[BUF_SIZE];
char addrStr[IS_ADDR_STR_LEN];
if (becomeDaemon(0) == -1)
errExit("becomeDaemon");
sfd = inetBind(SERVICE, SOCK_DGRAM, NULL);
if (sfd == -1) {
syslog(LOG_ERR, "Could not create server socket (%s)", strerror(errno));
exit(EXIT_FAILURE);
}
/* Receive datagrams and return copies to senders */
for (;;) {
len = sizeof(struct sockaddr_storage);
numRead = recvfrom(sfd, buf, BUF_SIZE, 0,
(struct sockaddr *) &claddr, &len);
if (numRead == -1)
errExit("recvfrom");
if (sendto(sfd, buf, numRead, 0, (struct sockaddr *) &claddr, len)
!= numRead)
syslog(LOG_WARNING, "Error echoing response to %s (%s)",
inetAddressStr((struct sockaddr *) &claddr, len,
addrStr, IS_ADDR_STR_LEN),
strerror(errno));
}
}
#include "id_echo.h"
int
main(int argc, char *argv[])
{
int sfd, j;
size_t len;
ssize_t numRead;
char buf[BUF_SIZE];
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s host msg...\n", argv[0]);
/* Construct server address from first command-line argument */
sfd = inetConnect(argv[1], SERVICE, SOCK_DGRAM);
if (sfd == -1)
fatal("Could not connect to server socket");
/* Send remaining command-line arguments to server as separate datagrams */
for (j = 2; j < argc; j++) {
len = strlen(argv[j]);
if (write(sfd, argv[j], len) != len)
fatal("partial/failed write");
numRead = read(sfd, buf, BUF_SIZE);
if (numRead == -1)
errExit("read");
printf("[%ld bytes] %.*s\n", (long) numRead, (int) numRead, buf);
}
exit(EXIT_SUCCESS);
}
(base) wannian07@wannian07-PC:~/Desktop/std/linux_prog_interface$ gcc id_echo_cl.c -o id_echo_cl error_functions.c become_daemon.c inet_sockets.c
(base) wannian07@wannian07-PC:~/Desktop/std/linux_prog_interface$ gcc id_echo_cl.c -o id_echo_cl error_functions.c become_daemon.c inet_sockets.c
60.3 并发型TCP echo服务器 1019
客户端发送无限量数据给服务器,适合并发型,使多个客户端能够同时得到服务
1.服务器调用becomeDaemon()成为一个守护进程
2.为使程序更小,使用Internet域套接字函数库
3.服务器为每个客户端传教一个子进程,确保不会出现僵尸进程。通过信号SIGCHLD安装信号处理例程来实现
4.服务器主体部分由for()循环组成,接受客户端的连接,通过fork()传教子进程。子进程调用handleRequest()处理客户端,父进程在for()循环下接受下一个客户端连接。
5.每次调用fork()后,监听套接字和连接套接字都在子进程中得到复制。
6.每个子进程在处理完一个客户端后终止。
客户端:
#include "inet_sockets.h"
#include "tlpi_hdr.h"
#define BUF_SIZE 100
int
main(int argc, char *argv[])
{
int sfd;
ssize_t numRead;
char buf[BUF_SIZE];
if (argc != 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s host\n", argv[0]);
sfd = inetConnect(argv[1], "echo", SOCK_STREAM);
if (sfd == -1)
errExit("inetConnect");
switch (fork()) {
case -1:
errExit("fork");
case 0: /* Child: read server's response, echo on stdout */
for (;;) {
numRead = read(sfd, buf, BUF_SIZE);
if (numRead <= 0) /* Exit on EOF or error */
break;
printf("%.*s", (int) numRead, buf);
}
exit(EXIT_SUCCESS);
default: /* Parent: write contents of stdin to socket */
for (;;) {
numRead = read(STDIN_FILENO, buf, BUF_SIZE);
if (numRead <= 0) /* Exit loop on EOF or error */
break;
if (write(sfd, buf, numRead) != numRead)
fatal("write() failed");
}
/* Close writing channel, so server sees EOF */
if (shutdown(sfd, SHUT_WR) == -1)
errExit("shutdown");
exit(EXIT_SUCCESS);
}
}
服务器:
#include <signal.h>
#include <syslog.h>
#include <sys/wait.h>
#include "become_daemon.h"
#include "inet_sockets.h" /* Declarations of inet*() socket functions */
#include "tlpi_hdr.h"
#define SERVICE "echo" /* Name of TCP service */
#define BUF_SIZE 4096
static void /* SIGCHLD handler to reap dead child processes */
grimReaper(int sig)
{
int savedErrno; /* Save 'errno' in case changed here */
savedErrno = errno;
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
errno = savedErrno;
}
/* Handle a client request: copy socket input back to socket */
static void
handleRequest(int cfd)
{
char buf[BUF_SIZE];
ssize_t numRead;
while ((numRead = read(cfd, buf, BUF_SIZE)) > 0) {
if (write(cfd, buf, numRead) != numRead) {
syslog(LOG_ERR, "write() failed: %s", strerror(errno));
exit(EXIT_FAILURE);
}
}
if (numRead == -1) {
syslog(LOG_ERR, "Error from read(): %s", strerror(errno));
exit(EXIT_FAILURE);
}
}
int
main(int argc, char *argv[])
{
int lfd, cfd; /* Listening and connected sockets */
struct sigaction sa;
if (becomeDaemon(0) == -1)
errExit("becomeDaemon");
/* Establish SIGCHLD handler to reap terminated child processes */
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = grimReaper;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
syslog(LOG_ERR, "Error from sigaction(): %s", strerror(errno));
exit(EXIT_FAILURE);
}
lfd = inetListen(SERVICE, 10, NULL);
if (lfd == -1) {
syslog(LOG_ERR, "Could not create server socket (%s)", strerror(errno));
exit(EXIT_FAILURE);
}
for (;;) {
cfd = accept(lfd, NULL, NULL); /* Wait for connection */
if (cfd == -1) {
syslog(LOG_ERR, "Failure in accept(): %s", strerror(errno));
exit(EXIT_FAILURE);
}
/* Handle each client request in a new child process */
switch (fork()) {
case -1:
syslog(LOG_ERR, "Can't create child (%s)", strerror(errno));
close(cfd); /* Give up on this client */
break; /* May be temporary; try next client */
case 0: /* Child */
close(lfd); /* Unneeded copy of listening socket */
handleRequest(cfd);
_exit(EXIT_SUCCESS);
default: /* Parent */
close(cfd); /* Unneeded copy of connected socket */
break; /* Loop to accept next connection */
}
}
}
(base) wannian07@wannian07-PC:~/Desktop/std/linux_prog_interface$ gcc is_echo_cl.c -o is_echo_cl error_functions.c become_daemon.c inet_sockets.c
(base) wannian07@wannian07-PC:~/Desktop/std/linux_prog_interface$ gcc is_echo_sv.c -o is_echo_sv error_functions.c become_daemon.c inet_sockets.c
60.4 并发型服务器的其他设计方案 1021
https://www.bilibili.com/read/cv28123863/
TCP多进程并发服务器(进程池版本)&TCP多进程并发服务器(动态创建进程版本)
TCP多线程并发服务器(进程池版本)&TCP多线程并发服务器(动态创建进程版本)
https://www.bilibili.com/read/cv28123863/
在服务器上预先创建进程或线程
1.服务器程序在启动阶段(即在任何客户端请求到来之前)就立刻预先创建好一定数量的子进程(或线程),而不是针对每个客户端来创建一个新的子进程(或线程)。这些子进程构成了一种服务池(server pool)
2.服务池中的每个子进程一次只处理一个客户端。在处理完客户端请求后,子进程并不会终止,而是获取下一个待处理的客户端继续处理,如此类推。
3.服务器应用中仔细地管理子进程。服务池应该足够大,以确保能充分响应客户端的请求。 a. 服务器父进程必须对未占用的子进程加以监视,服务器处于负载高峰期时增加服务池的大小,这样就总会有足够多的子进程存在,从而可以立刻服务于新的客户端请求。
b. 负载下降了,那么应该相应地降低服务池的大小,因为过多的空余进程会降低系统的整体性能。
c. 服务池中的子进程必须遵循某些协议,使得它们能以独占的方式选择一个客户端连接。
d. 服务池中的每个子进程在监听描述符的 accept()调用上阻塞就足够了。
e. 服务器父进程在创建任何子进程之前先创建监听套接字, 然后每个子进程在 fork()调用中继承该套接字的文件描述符。当一个新的客户端连接到来时,只有其中一个子进程能完成 accept()调用。
f. 由于 accept()在一些老式的实现中并不是一个原子化的系统调用,因此可能需要通过一些互斥技术(例如文件锁)来支持,以确保每次只有一个子进程可以执行 accept()调用
在单个进程中处理多个客户端
设计让单个服务器进程来处理多个客户端, 必须采用一种能允许单个进程同时监视多个文件描述符上 I/O 事件的 I/O 模型(I/O 多路复用、信号驱动 I/O 或者 epoll)。
在设计单进程服务器时,服务器进程必须做一些通常由内核来处理的调度任务。在每个客户端一个服务器进程地解决方案中,依靠内核来确保每个服务器进程能公平地访问到服务器主机的资源。用单个服务器进程来处理多个客户端时,服务器进程必须自行确保一个或多个客户端不会霸占服务器,从而使其他的客户端处于饥饿状态。
采用服务器集群
使用多个服务器系统—服务器集群(server farm)。构建服务器集群最简单的一种方法是 DNS 轮转负载共享(DNS round-robin load sharing)(或负载分发, load distribution),一个地区的域名权威服务器将同一个域名映射到多个 IP地址上(即,多台服务器共享同一个域名)。后续对 DNS 服务器的域名解析请求将以循环轮转的方式以不同的顺序返回这些 IP 地址。
优势: DNS 循环轮转的是成本低,而且容易实施。
问题1、是远端 DNS 服务器上所执行的缓存操作, 这意味着今后位于某个特定主机(或一组主机)上的客户端发出的请求会绕过循环轮转 DNS 服务器,并总是由同一个服务器来负责处理。
问题2、循环轮转 DNS 并没有任何内建的用来确保达到良好负载均衡(不同的客户端在服务器上产生的负载不同)或者是确保高可用性的机制(如果其中一台服务器宕机或者运行的服务器 程序崩溃了怎么办?)。
服务器亲和性(server affinity),确保来自同一个客户端的请求序列能够全部定向到同一台服务器上,这样由服务器维护的任何有关客户端状态的信息都能保持准确。
服务器负载均衡(server load balancing)由一台负载均衡服务器将客户端请求路由到服务器集群中的其中一个成员上。(为了确保高可用性,可能还会有一台备用的服务器。一旦负载均衡主服务器崩溃,备用服务器就立刻接管主服务器的任务。)这消除了由远端 DNS 缓存所引起的问题,因为服务器集群只对外提供了一个单独的 IP 地址(也就是负载均衡服务器的 IP 地址)。负载均衡服务器结合一些算法来衡量或计算服务器负载(可能是根据服务器集群的成员所提供的量值),并智能化地将负载分发到集群中的各个成员之上。负载均衡服务器也会自动检测集群中失效的成员(如果需要,还会自动检测新增加的服务器成员)。最后,负载均衡服务器可能还会提供对服务器亲和力的支持。
https://www.jb51.net/article/122310.htm
服务器负载均衡的基本功能和实现原理
60.5 inetd( Internet 超级服务器)守护进程
/etc/services , 列出了系统中服务项目,大部分服务器进程通常只是等待着偶尔发送过来的连接请求或数据报, 等待。服务器进程会占用内核进程表中的槽位,会占用一些内存和交换空间,对系统产生负载。
守护进程 inetd 用来消除运行大量非常用服务器进程的需要。好处:
1.与其为每个服务运行一个单独的守护进程,现在只用一个进程—inetd 守护进程—就可以监视一组指定的套接字端口,并按照需要启动其他的服务。因此可降低系统上运行的进程数量。
2.inetd 简化了启动其他服务的编程工作。因为由 inetd 执行的一些步骤通常在所有的网络服务启动时都会用到。 由于 inetd 监管着一系列的服务,可按照需要启动其他的服务,因此 inetd 有时候也被称为 Internet 超级服务器。
inetd 守护进程所做的操作
inetd 守护进程通常在系统启动时运行。 inetd 执行如下步骤:
1.对于在配置文件/etc/inetd.conf 中指定的每项服务, inetd 都会创建一个恰当类型的套接字(即流式套接字或数据报套接字),然后绑定到指定的端口号上。此外,每个 TCP套接字都会通过 listen()调用允许客户端发来连接。
2.通过 select()调用, inetd 对前一步中创建的所有套接字进行监视,看是否有数据报或请求连接发送过来
3.select()调用进入阻塞态,直到一个 UDP 套接字上有数据报可读或者 TCP 套接字上收到了连接请求。在 TCP 连接中, inetd 在进入下一个步骤之前会先为连接执行 accept()调用。
4.要启动这个套接字上指定的服务, inted 调用 fork()创建一个新的进程, 然后通过 exec()启动服务器程序。在执行 exec()前,子进程执行如下的步骤。
(a)除了用于 UDP 数据报和接受 TCP 连接的文件描述符外,将其他所有从父进程继 承而来的文件描述符都关闭。
(b)在文件描述符 0、 1 和 2 上复制套接字文件描 述符,并关闭套接字文件描述符本身(因为已经不需要它了)。完成这一步之后, 启动的服务器进程就能通过这三个标准的文件描述符同套接字通信了。
(c)为启动的服务器进程设定用户和组 ID,设定的值可在 /etc/inetd.conf 中的相应条目找到。
5.第 3 步中,如果在 TCP 套接字上接受了一个连接, inetd 就关闭这个连接套接字。
6.inetd 服务跳转回第 2 步继续执行。
/etc/inetd.conf 文件
inetd 守护进程的操作由一个配置文件来控制,通常是/etc/inetd.conf。该文件中的每一行都描述了一种由 inetd 处理的服务
echo 服务: /etc/inetd.conf 文件中的每一行都由以下字段组成,由空格来将它们分隔开。
1.服务名称(service name):指定了一项服务的名称,这项服务可在/etc/services 文件中找到。结合协议字段(protocol),就可以通过查找/etc/services 文件以确定 inetd 应该为这项服务监视哪一个端口号。
2.套接字类型(Socket type):该字段指定了这项服务所用的套接字类型—流式 套接字(stream)还是数据报套接字(dgram)。
3.协议( protocol):该字段指定了这个套接字所使用的协议。这个字段可以包含文件 /etc/protocols 中所列出的任何 Internet 协议,所有的服务都会指定 tcp或 udp。
4. 标记(flags):该字段的内容要么是 wait,要么是 nowait。这个字段指明了由 inetd 启 动的服务器(暂时的)是否会接管用于该服务的套接字。如果启动的服务器需要管理 这个套接字,那么该字段被指定为 wait。这将导致 inetd 把这个套接字从它所监视的文件描述符集合中移除,直到这个服务器 程序退出为止。
5.登录名( login name):该字段由/etc/passwd 中的用户名部分组成,还可以在其后 紧跟一个句号以及一个/etc/group 中的组名称。这些名称确定了运行的服务器程 序的用户 ID 和组 ID。
6.服务器程序(server program):该字段指定了被执行的服务器程序的路径名。
7.服务器程序参数(server program arguments):该字段指定了一个或多个参数,参数之间由空格符分隔。当执行服务器程序时,这些参数就作为程序的参数列表。在被执行的服务器程序中, 第一个参数对应于 argv[0], 通常和服务器程序名称的基础部分相同。 下一个参数对应于 argv[1],以此类推。
由 inetd 调用的流式套接字(TCP)服务器通常都被设计为只处理一个单独的客户端连接,处理完后就终止, 把监听其他连接的任务留给了 inetd。 修改了/etc/inetd.conf 文件后,需要发送一个 SIGHUP 信号给 inetd,请求它重新读取配置文件
inetd 可以简化服务器程序的编程工作,并发型(通常是 TCP)服务器。这是因为 inetd 已经帮它所调用的服务器程序完成了以下步骤。
1.执行所有和套接字相关的初始化工作,调用 socket()、 bind()以及 listen()(针对 TCP服务器)。
2.对于一个 TCP 服务,为新到来的连接执行 accept()操作。
3.创建一个新的进程来处理到来的 UDP 数据报或者是 TCP 连接。自动将调用的服务器进程设置为守护进程。 inetd 通过 fork()处理所有与进程创建相关的细节,通过 SIGCHLD 信号处理例程清除所有退出的子进程。
4.将代表 UDP 套接字或 TCP 连接套接字的文件描述符复制到标准文件描述符 0、 1 和2 上,并关闭所有其他的文件描述符(因为它们并不会在调用的服务器进程中用到)。
5.执行服务器程序。
在/etc/inetd.conf 文件中创建如下的条目,使得 inetd 可以调用该服务器程序
#include <syslog.h>
#include "tlpi_hdr.h"
#define BUF_SIZE 4096
int
main(int argc, char *argv[])
{
char buf[BUF_SIZE];
ssize_t numRead;
while ((numRead = read(STDIN_FILENO, buf, BUF_SIZE)) > 0) {
if (write(STDOUT_FILENO, buf, numRead) != numRead) {
syslog(LOG_ERR, "write() failed: %s", strerror(errno));
exit(EXIT_FAILURE);
}
}
if (numRead == -1) {
syslog(LOG_ERR, "Error from read(): %s", strerror(errno));
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
60.6 总结
https://www.cnblogs.com/wangbin2188/tag/Linux/