10 网络套接字
本节对应APUE第十六章——网络IPC:套接字
10.1 引言
上一章考察了各种UNIX系统所提供的经典进程间通信机制(IPC):管道、FIFO、消息队列、信号量以及共享存储。这些机制允许在同一台计算机上运行的进程可以相互通信。本章将考察不同计算机(通过网络相连)上的进程相互通信的机制:网络进程间通信(network IPC)。
在本章中,我们将描述套接字网络进程间通信接口,进程用该接口能够和其他进程通信,无论它们是在同一台计算机上还是在不同的计算机上。实际上,这正是套接字接口的设计目标之一:同样的接口既可以用于计算机间通信,也可以用于计算机内通信。尽管套接口可以采用许多不同的网络协议进行通信,但本章的讨论限制在因特网事实上的通信标准:TCP/IP协议栈。
10.2 套接字描述符
套接字是一种通信机制(通信的两方的一种约定),socket屏蔽了各个协议的通信细节,提供了tcp/ip协议的抽象,对外提供了一套接口,通过这个接口就可以统一、方便的使用tcp/ip协议的功能。这使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。我们可以用套接字中的相关函数来完成通信过程。
套接字是通信端点的抽象。正如使用文件描述符访问文件,应用程序用套接字描述符访问套接字。套接字描述符在UNIX系统中被当作是一种文件描述符。事实上,许多处理文件描述符的函数(如read和write)可以用于处理套接字描述符。
为创建一个套接字,调用socket函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:域,或者协议族,确定通信的特性。即采用什么协议来传输数据。
AF_UNIX、AF_LOCAL:本地协议;
AF_INET:IPV4 协议;
AF_INET6:IPV6 协议;
AF_IPX:是非常古老的操作系统,出现在 TCP/IP 之前;
AF_NETLINK:是用户态与内核态通信的协议;
AF_APPLETALK:苹果使用的一个局域网协议;
AF_PACKET:底层 socket 所用到的协议,比如抓包器所遵循的协议一定要在网卡驱动层,而不能在应用层,否则无法见到包封装的过程。再比如 ping 命令,想要实现 ping 命令就需要了解这个协议族。
type:确定套接字的类型,进一步确定通信特征。
SOCK_STREAM:流式套接字,特点是有序、可靠。有序、双工、基于连接的、以字节流为单位的。
可靠不是指不丢包,而是流式套接字保证只要你能接收到这个包,那么包中的数据的完整性一定是正确的。
双工是指双方都能收发。
基于连接的是指:通信双方是知道对方是谁。
字节流是指数据没有明显的界限,一端数据可以分为任意多个包发送。
SOCK_DGRAM:报式套接字(数据报套接字),无连接的,固定的最大长度,不可靠的消息。
SOCK_SEQPACKET:提供有序、可靠、双向、基于连接的数据报通信。
SOCK_RAW:原始的套接字,提供的是网络协议层的访问。
SOCK_RDM:数据层的访问,不保证传输顺序。
protocol:具体使用哪个协议。在 domain 的协议族中每一个对应的 type 都有一个或多个协议,使用协议族中默认的协议可以填写0。
返回值:如果成功,返回套接字描述符;如果失败,返回 -1,并设置 errno。
调用socket与调用open相类似。在两种情况下,均可获得用于I/O的文件描述符。当不再需要该文件描述符时,调用close来关闭对文件或套接字的访问,并且释放该描述符以便重新使用。
虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符。图16-4总结了到目前为止所讨论的大多数以文件描述符为参数的函数使用套接字描述符时的行为。未指定和由实现定义的行为通常意味着该函数对套接字描述符无效。例如lseek不能以套接字描述符为参数,因为套接字不支持文件偏移量的概念。
10.3 制定协议
10.3.1 字节序
大端格式:低地址存放高位数据,高地址存放低位数据。
小端格式:低地址存放低位数据,高地址存放高位数据。
假设要存放的数据是 0x30313233,那么 33 是低位,30 是高位,在大端存储格式中,30 存放在低位,33 存放在高位;而在小端存储格式中,33 存放在低位,30 存放在高位。
主机字节序:host
网络字节序:network
网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会被字节序所混淆。TCP/IP协议栈使用大端字节序。应用程序交换格式化数据时,字节序问题就会出现。对于TCP/IP,地址用网络字节序来表示,所以应用程序有时需要在处理器的字节序与网络字节序之间转换它们。例如,以一种易读的形式打印一个地址时,这种转换很常见。
对于TCP/IP应用程序,有4个用来在处理器字节序和网络字节序之间实施转换的函数:
#include <arpa/inet.h>
// 主机字节序转换为网络字节序
// 返回以网络字节序表示的32位整数
uint32_t htonl(uint32_t hostlong);
// 主机字节序转换为网络字节序
// 返回以网络字节序表示的16位整数
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h表示主机字节序,
n表示网络字节序。
l表示长(即4字节)整数
s表示短(即2字节)整数
10.3.2 对齐
以下面的结构体为例:
struct {
int i;
float f;
char ch;
};
在32位的机器上,各占用4,4,1共9个字节的大小。但是编译器会将其自动对齐,此时为12个字节。
对齐原因:
平台原因: 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。优点是提高了可移植性和cpu性能。
网络传输的结构体中的成员都是紧凑的,所以不能地址对齐,需要在结构体外面增加 __attribute__((packed))。例如:
struct msg_st {
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
}__attribute__((packed));
10.3.3 类型长度问题
标准C并没有对int、char这样的基本数据类型占用多大字节做一个明确的规定,例如:
一个16位的机器上,int可能占2个字节;
一个32位的机器上,int可能占4个字节;
解决:使用int32_t、uint32_t、int64_t、int8_t、uint8_t等类型明确指定占用的位数。这些类型包含在头文件<stdint.h>中。
stdint.h
stdint.h是c99中引进的一个标准C库的头文件。
C的整数类型有:
类型 | 最小大小 | 说明 |
char | 无要求 | 最小的可寻址单元,能包含基础字符集,大小无硬性要求,可能是signed或unsigned的,现在基本都是8 bits |
short | 16 | |
int | 16 | 没有要求要大于short |
long | 32 | |
long long | 64 | 没有要求要大于short |
每个类型都有signed和unsigned之分。
C99中引入了固定大小的整数类型,和字节数有关的类型,其中包括:
定长类型(u)intN_t,比如 int16_t,uint64_t ,保证变量占用的内存空间一定,但是不保证能够无开销的进行无符号整数溢出,也不保证在任何平台和编译器中存在。
快速运算类型(u)int_fastN_t,比如int_fast32_t,会根据平台来选择运算速度最快的大于N比特的类型,通常在AMD64下当N大于等于16时都是 (u)int64_t,这样就没有必要保证完美的进行无开销的无符号整数溢出的行为,并且保证在任何编译器实现中都存在。通常它们都用于在寄存器中的运算,你绝对不会想把它们存在内存里,这就要看:
空间优先类型(u)int_leastN_t,比如int_least16_t,会从定长类型中选择满足最小的至少为N的类型,保证一定存在,不保证一定为N比特.
10.3.4 主动端和被动端
Socket中主动端(客户端)和被动端(服务器)需要做的工作有:
主动端——客户端
1.取得 Socket
2.给 Socket 关联绑定地址(可省略,不必与操作系统约定端口,由操作系统指定随机端口)
3.发/收消息
4.关闭 Socket
被动端——服务器
1.取得 Socket
2.给 Socket 关联地址
3.将Socket置为监听模式(如果是流式套接字)
4.接受连接(如果是流式套接字)
5.收/发消息
6.关闭 Socket
10.4 寻址
标识目标通信进程需要网络地址(IP)和端口号(port),前者标识网络上想与之通信的计算机,后者帮助标识特定的进程,因此需要将套接字与这两者进行绑定关联。
10.4.1 地址格式
一个地址(IP+PORT)标识一个特定通信域的套接字端点,地址格式与这个特定的通信域相关。为使不同格式地址能够传入到套接字函数,不同的地址结构会被强制转换成一个通用的地址结构 sockaddr:
struct sockaddr {
sa_family_t sa_family; // 协议族
char sa_data[]; // 变长地址
// ...
};
例如,因特网地址定义在<netinet/in.h>头文件中。在IPv4因特网域(AF_INET)中,套接字的地址用结构 sockaddr_in表示:
// 地址
struct sockaddr_in {
sa_family_t sin_family; // 协议族
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // ipv4地址
};
struct in_addr {
uint32_t s_addr; // ipv4地址,是一个32位无符号的整数
};
有时,需要打印出能被人理解而不是计算机所理解的地址格式。BSD 网络软件包含函数inet_addr和inet_ntoa,用于二进制地址格式与点分十进制字符表示(a.b.c.d)之间的相互转换。但是这些函数仅适用于IPv4地址。有两个新函数inet_ntop和inet_pton具有相似的功能,而且同时支持IPv4地址和IPv6地址。
#include <arpa/inet.h>
// 将网络字节序的地址addr(一个uint32_t的大整数)转换为点分十进制的文本字符串格式str
const char *inet_ntop(int domain, const void *restrcit addr, char *restrict str, socklen_t size);
// 将点分十进制的文本字符串格式转换为网络字节序的地址(一个uint32_t的大整数),放在addr中
const char *inet_pton(int domain, const void *restrcit str, char *restrict addr);
domain:协议族,仅支持以下两种:
AF_INET:IPV4 协议
AF_INET6:IPV6 协议
addr:网络字节序的二进制地址
str和size:指定存放转换后地址的位置和缓冲区大小,size的大小可设置为下面两个常数
INET_ADDRSTRLEN:它定义了足够大的空间来存放一个表示IPv4地址的文本字符串
INET6_ADDRSTRLEN :定义了足够大的空间来存放一个表示 IPv6 地址的文本字符串
10.4.2 套接字和地址关联
将一个客户端的套接字关联上一个地址没有多少新意,可以让系统选一个默认的地址。然而,对于服务器,需要给一个接收客户端请求的服务器套接字关联上一个众所周知的地址。客户端应有一种方法来发现连接服务器所需要的地址,最简单的方法就是服务器保留一个地址并且注册在/etc/services或者某个名字服务中。
使用bind函数来关联地址和套接字:
// bind - bind a name to a socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符
addr:要绑定到套接字上的地址。该地址必须和创建套接字时的地址族所支持的格式相匹配。例如,创建套接字时指定的domain为AF_INET(ipv4),则传入的地址的结构体类型必须为sockaddr_in,详见11.4.1节
addrlen:addr 传递的地址结构体的长度。
可以调用 getsockname函数来发现绑定到套接字上的地址:
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
如果套接字已经和对等方连接,可以调用 getpeername 函数来找到对方的地址:
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
10.5 建立连接
如果要处理一个面向连接(流套接字)的网络服务,那么在开始交换数据以前,需要在请求服务的进程套接字(客户端)和提供服务的进程套接字(服务器)之间建立一个连接。
使用connect函数来建立连接:
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
// 成功返回0, 错误返回-1
在connect中指定的地址是我们想与之通信的服务器地址。如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址。
服务器调用 listen 函数来宣告它愿意接受连接请求。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 成功返回0, 错误返回-1
参数 backlog 提供了一个提示,提示系统该进程所要入队的未完成连接请求数量。其实际值由系统决定,但上限由<sys/socket.h>中的SOMAXCONN指定。
一旦队列满,系统就会拒绝多余的连接请求,所以 backlog 的值应该基于服务器期望负载和处理量来选择,其中处理量是指接受连接请求与启动服务的数量。
一旦服务器调用了 listen,所用的套接字就能接收连接请求。
使用accept函数获得连接请求并建立连接。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
// 成功返回套接字描述符,失败返回-1
函数accept所返回的文件描述符是套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字(sockfd)具有相同的套接字类型和地址族。但是传给 accept 的原始套接字没有关联到这个连接,而是继续保持可用状态并接收其他连接请求。
如果不关心客户端标识,可以将参数addr(对端地址)和len设为NULL。否则,在调用accept之前,将addr参数设为足够大的缓冲区来存放地址,并且将len指向的整数设为这个缓冲区的字节大小。返回时,accept会在缓冲区填充客户端的地址,并且更新指向len的整数来反映该地址的大小。
如果没有连接请求在等待,accept 会阻塞直到一个请求到来。如果 sockfd处于非阻塞模式,accept 会返回-1,并将 errno 设置为 EAGAIN。
如果服务器调用 accept,并且当前没有连接请求,服务器会阻塞直到一个请求到来,另外,服务器可以使用 poll或 select 来等待一个请求的到来。在这种情况下,一个带有等待连接请求的套接字会以可读的方式出现。
10.6 套接字选项
套接字机制提供了两个套接字选项接口来控制套接字行为。一个接口用来设置选项,另一个接口可以查询选项的状态。
#include <sys/socket.h>
// 成功返回0,失败返回-1
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
level: 标识了选项应用的协议。
如果选项是通用的套接字层次选项,则 level 设置成SOL_SOCKET。否则,level设置成控制这个选项的协议编号
对于TCP,level是IPPROTO_TCP
对于IP,level是IPPROTO_IP。
下图总结了通用套接字层次选项。
optname: 需设置的选项
optval:根据选项的不同指向一个数据结构或者一个整数。一些选项是on/off开关。如果整数非0,则启用选项。如果整数为0,则禁止选项。
optlen:指定了optval指向的对象的大小。
可以使用 getsockopt 函数来查看选项的当前值:
#include <sys/socket.h>
// 成功返回0,失败返回-1
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
10.7 数据传输
10.7.1 发送和接收
发送数据,类似于write:
#include <sys/types.h>
#include <sys/socket.h>
// 用于有连接的流套接字
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 可以在无连接的报式套接字上指定一个目标地址
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// 返回值是真正发送出去的数据的长度;出现错误返回 -1 并设置 errno。
注意:即使 send 成功返回,也并不表示连接的另一端的进程就一定接收了数据。我们所能保证的只是当send成功返回时,数据已经被无错误地发送到网络驱动程序上。
接收数据,类似于read:
#include <sys/types.h>
#include <sys/socket.h>
// 流套接字
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 报套接字
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
// 返回读到的字节数,若无可读数据或对等方已经结束,返回0,出错返回-1
recv 函数一般用在流式(SOCK_STREAM)套接字中,而 recvfrom 则一般用在报式(SOCK_DGRAM)套接字中。
10.7.2 报式套接字示例
① 基本实现
proto.h
/* proto.h */
#ifndef __PROTO_H__
#define __PROTO_H__
#include <stdint.h>
#define RCVPORT "1989" // 服务器的端口号,使用到时用atoi进行转换,原因:没有单位的数字1989没有意义,因此用字符串来代替
#define NAMESIZE 13 //为了测试数据对齐的问题,这里选择一个一定不对齐的数字
// 传输的结构体
struct msg_st {
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
}__attribute__((packed)); //告诉gcc编译器,不要对齐
#endif
rcver.c
/* rcver.c */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "proto.h"
#define IPSTRSIZE 64
int main(void) {
// 套接字
int sd;
// laddr -- local address -- 本机地址
// raddr -- remote address -- 对端地址
struct sockaddr_in laddr,raddr;
// 对端地址长度
socklen_t raddr_len;
// 接收到的结构体
struct msg_st rbuf;
// 存储点分十进制字符串的数组
char ipstr[IPSTRSIZE];
// 创建协议为ipv4的报式套接字,0为默认协议,即UDP
sd = socket(AF_INET, SOCK_DGRAM, 0/*IPPROTO_UDP*/);
if(sd < 0) {
perror("socket()");
exit(1);
}
// 本机地址的配置
laddr.sin_family = AF_INET;
// ip地址和网络端口号是要通过网络发送过去的,所以需要考虑字节序的问题,也就是htons
laddr.sin_port = htons(atoi(RCVPORT));
// 因为本机的ip地址有可能会变化,为了避免ip地址每一次变化,都要进来修改,所以给它匹配一个万能地址0.0.0.0
// 对"0.0.0.0"的定义是any address.就是说在当前绑定阶段,本机的ip地址是多少,这四个0就会自动换成当前的ip地址.
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr.s_addr);
// 关联地址和套接字
if(bind(sd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
perror("bind()");
exit(1);
}
// 这里一定要初始化对端地址的大小!
raddr_len = sizeof(raddr);
while(1) {
if(recvfrom(sd, &rbuf, sizeof(rbuf), 0, (void *)&raddr, &raddr_len) < 0) {
perror("recvfrom()");
exit(1);
}
// 整数转点分十进制的字符串
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
printf("---MESSAGE FROM %s:%d---\n", ipstr, ntohs(raddr.sin_port));
// 单字节传输不涉及到大端小端的存储情况
printf("Name = %s\n",rbuf.name);
printf("Math = %d\n",ntohl(rbuf.math));
printf("Chinese = %d\n",ntohl(rbuf.chinese));
}
// 关闭套接字
close(sd);
exit(0);
}
运行编译好的代码,然后,重启一个终端,使用命令netstat -anu来查看,其中,u代表的是udp。
[root@zoukangcheng socket]# netstat -anu
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
udp 0 0 0.0.0.0:1989 0.0.0.0:*
udp 0 0 0.0.0.0:68 0.0.0.0:*
udp 0 0 10.0.24.5:123 0.0.0.0:*
udp 0 0 127.0.0.1:123 0.0.0.0:*
udp6 0 0 fe80::5054:ff:fe2f::123 :::*
udp6 0 0 ::1:123 :::*
snder.c:这端可以不用向操作系统绑定端口,发送数据的时候由操作系统为我们分配可用的端口即可,当然如果想要自己绑定特定的端口也是可以的。
/* snder.c */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include "proto.h"
int main(int argc,char **argv) {
int sd;
// 发送的结构体
struct msg_st sbuf;
// 对端地址
struct sockaddr_in raddr;
if(argc < 2) {
fprintf(stderr,"Usage...\n");
exit(1);
}
// 创建套接字
sd = socket(AF_INET, SOCK_DGRAM, 0);
if(sd < 0) {
perror("socket()");
exit(1);
}
// bind(); // 主动端可省略绑定端口的步骤
memset(&sbuf, '\0', sizeof(sbuf));
strcpy(sbuf.name, "Alan");
sbuf.math = htonl(rand()%100);
sbuf.chinese = htonl(rand()%100);
// 对端地址的配置
raddr.sin_family = AF_INET; // 对端协议
raddr.sin_port = htons(atoi(RCVPORT)); // 对端端口
inet_pton(AF_INET, argv[1], &raddr.sin_addr); // 对端ip地址
// 发送数据
if(sendto(sd, &sbuf, sizeof(sbuf), 0, (void *)&raddr, sizeof(raddr)) < 0) {
perror("sendto()");
exit(1);
}
puts("OK!");
close(sd);
exit(0);
}
执行结果:
[root@zoukangchen socket]# ./snder 127.0.0.1
OK!
[root@zoukangchen socket]# ./rcver
---MESSAGE FROM 127.0.0.1:46573---
Name = Alan
Math = 83
Chinese = 86
② 动态报式套接字
传输的结构体中含有变长的数组。例如结构体中name数组的大小是不固定,此时可以使用变长结构体。
变长结构体
变长结构体是由gcc扩展的一种技术,它是指其最后一个成员的长度不固定(flexible array member,也叫柔性数组)。
使用范围:数据长度不固定,例如协议对接中有固定的头结构体,数据结构体不固定。
问题引出:项目中用到数据包的处理,但包的大小是不固定的,其长度由包头的2字节决定。比如如下的包头:
88 0f 0a ob cd ef 23 00
长度由头2个字节880f决定,考虑字节序,转为0f88,转为10进制为3976个字节的包长度。
这个时候存储包的时候,一方面可以考虑设定包的大小固定:如4K=4*1024=4096个字节,因为最大包长不可能超过4k,但该方法的有缺陷,存在一种极端就是包最小仅含包头不含数据域,此时包为8个字节,浪费了4096-8 =4088个字节的存储空间。
另一方面考虑有没有一种方法能根据长度进行存储,或者说初始不分配长度,计算出了长度后再分配存储呢。而实际项目中正是通过包头计算出了包的整体大小的。
这就引出了变长结构体的概念。例如:
struct Var_Len_Struct
{
int nsize;
char buffer[0];
// 或者不指定大小 char buffer[];
};
那结构体是怎么实现可变长的呢?如上所示,请注意看结构体中的最后一个元素:一个没有元素的数组(柔性数组)。我们可以通过动态开辟一个比结构体大的空间,然后让buffer去指向那些额外的空间,这样就可以实现可变长的结构体了。更为巧妙的是,我们甚至可以用nsize存储字符串buffer的长度。
代码示例:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
struct packet_st {
uint32_t packetHead; // 占4个字节
uint8_t packetData[0];
}__attribute__((packed));
int main() {
// 结构体长度
int size;
// 结构体指针
struct packet_st *bufp;
printf("sizeof(packet_st) = %d\n", sizeof(struct packet_st));
// 要发送的数据
char data[] = "123456";
// 发送的结构体长度: 结构体定长 + 要发送数据的长度
size = sizeof(struct packet_st) + strlen(data);
// 以长度申请动态内存
bufp = (struct packet_st*)malloc(size);
// 设置结构体成员
bufp->packetHead = 1;
strcpy(bufp->packetData , data);
// 发送数据的操作
// ...
printf("packetHead: %d\n", bufp->packetHead);
printf("packetData: %s\n", bufp->packetData);
free(bufp);
return 0;
}
执行结果:
[root@zoukangchen socket]# ./val_struct
sizeof(packet_st) = 4
packetHead: 1
packetData: 123456
可以看到,packetData在结构体中虽然定义长度为0,但仍然可以填充数据。
之前的代码修改如下:
proto.h
/* proto.h */
#ifndef __PROTO_H__
#define __PROTO_H__
#include <stdint.h>
#define RCVPORT "1989"
#define NAMEMAX (512-8-8) // 512为udp包推荐的字节数,8为udp的报头大小,8为结构体中固定长度的大小,即math和chinese
// 传输的变长结构体
struct msg_st {
uint32_t math;
uint32_t chinese;
uint8_t name[1]; // 1仅为占位符,数组定义一定要放在最后
}__attribute__((packed));
#endif
snder.c:发送方发送的结构体大小和内容是动态的,根据用户的命令行参数来确定
/* snder.c */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include "proto.h"
int main(int argc,char **argv) {
int sd;
// 结构体指针
struct msg_st *sbufp;
// 对端地址
struct sockaddr_in raddr;
// 发送结构体的大小
int size;
// Usage: ./snder IP NAME
if(argc < 3) {
fprintf(stderr,"Usage...\n");
exit(1);
}
if(strlen(argv[2]) > NAMEMAX) {
fprintf(stderr, "Name is to long!");
exit(1);
}
sd = socket(AF_INET, SOCK_DGRAM, 0);
if(sd < 0) {
perror("socket()");
exit(1);
}
// 结构体定长和可变名字的长度
size = sizeof(struct msg_st) + strlen(argv[2]);
// 申请动态内存
sbufp = (struct msg_st *)malloc(size);
if(sbufp == NULL) {
perror("malloc()");
exit(1);
}
strcpy(sbufp->name, argv[2]);
sbufp->math = htonl(rand()%100);
sbufp->chinese = htonl(rand()%100);
// 对端地址的配置
raddr.sin_family = AF_INET; // 对端协议
raddr.sin_port = htons(atoi(RCVPORT)); // 对端端口
inet_pton(AF_INET, argv[1], &raddr.sin_addr); // 对端ip地址
// 发送数据
if(sendto(sd, sbufp, size, 0, (void *)&raddr, sizeof(raddr)) < 0) {
perror("sendto()");
exit(1);
}
puts("OK!");
close(sd);
exit(0);
}
rcver.c:接收方不知道发送方发送的内容大小
/* rcver.c */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "proto.h"
#define IPSTRSIZE 64
int main(void) {
// 套接字
int sd;
// laddr -- local address -- 本机地址
// raddr -- remote address -- 对端地址
struct sockaddr_in laddr,raddr;
// 对端地址长度
socklen_t raddr_len;
// 结构体指针
struct msg_st *rbufp;
// 存储点分十进制字符串的数组
char ipstr[IPSTRSIZE];
int size;
size = sizeof(struct msg_st) + NAMEMAX - 1; // 1是name的占位大小
rbufp = (struct msg_st*)malloc(size);
sd = socket(AF_INET, SOCK_DGRAM, 0/*IPPROTO_UDP*/);
if(sd < 0) {
perror("socket()");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(RCVPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr.s_addr);
if(bind(sd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
perror("bind()");
exit(1);
}
raddr_len = sizeof(raddr);
while(1) {
if(recvfrom(sd, rbufp, size, 0, (void *)&raddr, &raddr_len) < 0) {
perror("recvfrom()");
exit(1);
}
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
printf("---MESSAGE FROM %s:%d---\n", ipstr, ntohs(raddr.sin_port));
printf("Name = %s\n",rbufp->name);
printf("Math = %d\n",ntohl(rbufp->math));
printf("Chinese = %d\n",ntohl(rbufp->chinese));
}
close(sd);
exit(0);
}
执行结果:
[root@zoukangchen socket]# ./snder 127.0.0.1 Mark
OK!
[root@zoukangchen socket]# ./rcver
---MESSAGE FROM 127.0.0.1:52874---
Name = Mark
Math = 83
Chinese = 86
③ 广播
在使用TCP/IP 协议的网络中,主机标识段host ID 为全1 的 IP 地址为广播地址。
广播数据有如下特点:
TCP/IP协议栈中,传输层只有UDP可以广播,TCP没有广播的概念.
UDP广播不需要经过路由器转发,因为路由器不会转发广播数据;
代码示例
snder.c:设置套接字,打开广播选项,并向广播地址255.255.255.255发送数据报
/* snder.c */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include "proto.h"
int main(int argc,char **argv) {
// ...
int val = 1;
// 设置该套接字,打开广播
if(setsockopt(sd, SOL_SOCKET, SO_BROADCAST, &val, sizeof(val)) < 0) {
perror("setsockopt()");
exit(1);
}
// ...
inet_pton(AF_INET, "255.255.255.255", &raddr.sin_addr); // 对端ip地址
// ...
}
打印结果:
[root@zoukangchen dgram]# ./snder Mark
OK!
[root@zoukangchen dgram]# ./snder Mary
OK!
[root@zoukangchen dgram]# ./rcver
---MESSAGE FROM 10.0.24.5:52621---
Name = Mark
Math = 83
Chinese = 86
---MESSAGE FROM 10.0.24.5:42769---
Name = Mary
Math = 83
Chinese = 86
④ 多播/组播
多播地址,也叫组播地址,组播报文的目的地址使用D类IP地址, D类地址不能出现在IP报文的源IP地址字段。组播地址可以分为四类:
224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;
224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet;
224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效;
239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。
代码示例
多播选项在IP协议中。相关数据结构和选项可以通过man 7 ip查看。
proto.h:设置一个约定的多播地址
// ...
#define MTGOURP "224.2.2.2" // 约定的多播组ip
// ...
snder.c:创建多播组
#include "proto.h"
int main(int argc,char **argv) {
// ...
// 创建多播组选项IP_MULTICAST_IF需要传入的数据结构
struct ip_mreqn mreq;
inet_pton(AF_INET, MTGOURP, &mreq.imr_multiaddr); // 多播地址
inet_pton(AF_INET, "0.0.0.0", &mreq.imr_address); // 自己的地址
mreq.imr_ifindex = if_nametoindex("eth0"); // 网络设备的索引号
// 设置该套接字(协议为IP,即IPPROTO_IP),创建多播组
if(setsockopt(sd, IPPROTO_IP, IP_MULTICAST_IF, &mreq, sizeof(mreq)) < 0) {
perror("setsockopt()");
exit(1);
}
// ...
inet_pton(AF_INET, MTGOURP, &raddr.sin_addr); // 对端ip地址
// ...
}
可以通过下列函数来获取网络设备名的索引编号:
#include <net/if.h>
unsigned int if_nametoindex(const char *ifname);
rcver.v:加入多播组
#include "proto.h"
#define IPSTRSIZE 64
int main(void) {
// ...
// 加入多播组选项IP_ADD_MEMBERSHIP需要传入的数据结构
struct ip_mreqn mreq;
inet_pton(AF_INET, MTGOURP, &mreq.imr_multiaddr); // 多播地址
inet_pton(AF_INET, "0.0.0.0", &mreq.imr_address); // 自己的地址
mreq.imr_ifindex = if_nametoindex("eth0"); // 网络设备的索引号
// 设置该套接字(协议为IP,即IPPROTO_IP),加入多播组
if(setsockopt(sd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
perror("setsockopt()");
exit(1);
}
// ...
}
执行结果:
[root@zoukangchen mcast]# ./snder Mark
OK!
[root@zoukangchen mcast]# ./snder Mike
OK!
[root@zoukangchen mcast]# ./rcver
---MESSAGE FROM 10.0.24.5:51081---
Name = Mark
Math = 83
Chinese = 86
---MESSAGE FROM 10.0.24.5:53772---
Name = Mike
Math = 83
Chinese = 86
多播中有一个特殊的 ip 地址(224.0.0.1),它表示,所有支持多播的地址默认都存在这个组当中,并且无法离开。如果 snder 方向这个 ip 地址发送信息,就相当于向 255.255.255.255 上发消息。
10.7.3 流式套接字示例
主动端和被动端的工作详见10.3.4小节
① 基本实现
proto.h
#ifndef __PROTO_H__
#define __PROTO_H__
#define SERVERPORT "1989" // 服务器端口
#define FMT_STAMP "%lld\r\n" // 格式化参数
#endif
server.c
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <time.h>
#include "proto.h"
#define IPSTRSIZE 40
#define BUFSIZE 1024
static void server_job(int sd) {
char buf[BUFSIZE];
int len;
// 将格式化数据写入到buf中,返回写入的字符总数
len = sprintf(buf, FMT_STAMP, (long long)time(NULL));
if(send(sd, buf, len, 0) < 0) {
perror("send()");
exit(1);
}
}
int main(void) {
int sd;
int new_sd;
struct sockaddr_in laddr, raddr;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
// 采用流式套接字SOCK_STREAM
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd < 0) {
perror("socket()");
exit(1);
}
int val = 1;
if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
perror("setsockopt()");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr.s_addr);
// 绑定地址
if(bind(sd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
perror("bind()");
exit(1);
}
// 监听连接
if(listen(sd, 200) < 0) {
perror("listen");
exit(1);
}
raddr_len = sizeof(raddr);
while(1) {
// 接收连接
new_sd = accept(sd, (void *)&raddr, &raddr_len);
if(new_sd < 0) {
perror("accept()");
exit(1);
}
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
printf("Client:%s:%d\n", ipstr, ntohs(raddr.sin_port));
server_job(new_sd);
close(new_sd);
}
close(sd);
exit(0);
}
使用命令,查看是否成功,t代表tcp
netstat -ant
发现已经处于监听状态。
可以使用nc命令来与服务器端建立连接:
nc 127.0.0.1 1989
执行结果:
[root@zoukangche basic]# nc 127.0.0.1 1989
1678513397
[root@zoukangche basic]# ./server
Client:127.0.0.1:36270
现在实现客户端的功能,其实也就是nc命令的功能。这里采用将系统io转换为标准io的指针来实现,当然,也可以采用recv来实现。
client.c
/* client.c */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include "proto.h"
int main(int argc, char*argv[]) {
int sd;
// 对端地址
struct sockaddr_in raddr;
FILE *fp;
long long stamp;
// Usage: ./client IP
if(argc < 2) {
fprintf(stderr,"Usage...\n");
exit(1);
}
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd < 0) {
perror("socket()");
exit(1);
}
// 对端地址的配置
raddr.sin_family = AF_INET; // 对端协议
raddr.sin_port = htons(atoi(SERVERPORT)); // 对端端口
inet_pton(AF_INET, argv[1], &raddr.sin_addr); // 对端ip地址
// 与对端建立连接
if(connect(sd, (void *)&raddr, sizeof(raddr)) < 0) {
perror("connect()");
exit(1);
}
// 系统io转换为标准io
// r+表示打开可读写的文件,且该文件必须存在
fp = fdopen(sd, "r+");
if(fp == NULL) {
perror("dfopen()");
exit(1);
}
// 根据数据格式FMT_STAMP从fp中读取数据到stamp中
if(fscanf(fp, FMT_STAMP, &stamp) < 1) {
fprintf(stderr, "Bad format!\n");
} else {
fprintf(stdout, "stamp = %lld\n", stamp);
}
// 按照标准io的方式关闭fp
fclose(fp);
exit(0);
}
fdopen的使用详见3.5节。
执行结果:
[root@zoukangche basic]# ./client 127.0.0.1
stamp = 1678514756
[root@zoukangche basic]# ./server
Client:127.0.0.1:37820
② 并发实现
服务器端fork出子进程来执行任务。
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <time.h>
#include "proto.h"
#define IPSTRSIZE 40
#define BUFSIZE 1024
static void server_job(int sd) {
// 假设很耗时
// ...
}
int main(void) {
// ...
pid_t pid;
// ...
while(1) {
// 父进程只接收连接
new_sd = accept(sd, (void *)&raddr, &raddr_len);
if(new_sd < 0) {
perror("accept()");
exit(1);
}
pid = fork(); // 创建子进程
if(pid) {
perror("fork()");
exit(1);
}
if(pid == 0) { // 子进程
close(sd); // 关闭不需要的主套接字(监听套接字)
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
printf("Client:%s:%d\n", ipstr, ntohs(raddr.sin_port));
server_job(new_sd); // 干活
close(new_sd);
exit(0); // 干完活后退出
}
close(new_sd); // 关闭不需要的副套接字
}
close(sd);
exit(0);
}
这里需要关闭不需要的套接字,详见6.2.1节的父子进程之间的文件共享。
原因在于 fork 执行之后,所有已经打开的套接字都被增加了引用计数,在其中任一个进程中都无法彻底关闭套接字,只能减少该文件的引用计数。因此,在 fork 之后,每个进程立即关闭不再需要的文件是个好的策略,否则很容易导致大量没有正确关闭的文件一直占用系统资源的现象。
③ 请求http服务实现
客户端功能:使用http的GET请求,下载服务器端上的一张图片。
服务器端准备
使用nginx作为web服务器,根目录一般位于usr/local/nginx。
将静态文件,例如test.jpg放置于根目录下的img文件夹(自己新建)中。
编写usr/local/nginx/conf/nginx.conf配置文件,在server块中新增映射关系,以响应请求静态文件的请求:
location ~* \.(gif|jpg|jpeg|png)$ { # url为gif|jpg|jpeg|png结尾时
root img; # 以root方式设置资源路径
}
例如,访问http://xxx.xxx.xxx.xxx:80/test.jpg时,匹配到末尾的jpg,nginx将其替换为/img/test.jpg。
client.c
/* client.c */
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
// #include "proto.h"
#define BUFSIZE 1024
int main(int argc, char*argv[]) {
int sd;
// 对端地址
struct sockaddr_in raddr;
FILE *fp;
char rbuf[BUFSIZE];
int len;
// Usage: ./client IP
if(argc < 2) {
fprintf(stderr,"Usage...\n");
exit(1);
}
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd < 0) {
perror("socket()");
exit(1);
}
// 对端地址的配置
raddr.sin_family = AF_INET; // 对端协议
raddr.sin_port = htons(80); // 对端端口,http一般为80端口
inet_pton(AF_INET, argv[1], &raddr.sin_addr); // 对端ip地址
// 与对端建立连接
if(connect(sd, (void *)&raddr, sizeof(raddr)) < 0) {
perror("connect()");
exit(1);
}
fp = fdopen(sd, "r+");
if(fp == NULL) {
perror("dfopen()");
exit(1);
}
// 发送http请求
fprintf(fp, "GET /test.jpg\r\n\r\n");
// 发送后刷新流
fflush(fp);
while(1) {
len = fread(rbuf, 1, BUFSIZE, fp);
if(len == 0) {
break;
}
// 打印在标准终端
fwrite(rbuf, 1, len, stdout);
}
fclose(fp);
exit(0);
}
执行结果:
[root@zoukangche basic]# ./webdl XXX.XXX.XXX.XXX > /tmp/out
④ 静态进程池套接字实现
静态进程池:父进程先fork出4个子进程,再由子进程来接收连接,处理任务。
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <time.h>
#include "proto.h"
#define IPSTRSIZE 40
#define BUFSIZE 1024
#define PROCNUM 4 // 静态进程池中进程的个数
static void server_job(int sd) {
char buf[BUFSIZE];
int len;
// 将格式化数据写入到buf中,返回写入的字符总数
len = sprintf(buf, FMT_STAMP, (long long)time(NULL));
if(send(sd, buf, len, 0) < 0) {
perror("send()");
exit(1);
}
}
static void server_loop(int sd) {
struct sockaddr_in raddr;
socklen_t raddr_len;
int new_sd;
char ipstr[IPSTRSIZE];
raddr_len = sizeof(raddr);
// 不断接收连接,处理任务
while(1) {
// 接收连接
new_sd = accept(sd, (void *)&raddr, &raddr_len);
if(new_sd < 0) {
perror("accept()");
exit(1);
}
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
printf("[%d]Client:%s:%d\n", getpid(), ipstr, ntohs(raddr.sin_port));
// 处理任务
server_job(new_sd);
close(new_sd);
}
// nerver reach
close(sd);
}
int main(void) {
int sd;
struct sockaddr_in laddr;
pid_t pid;
// 采用流式套接字SOCK_STREAM
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd < 0) {
perror("socket()");
exit(1);
}
int val = 1;
if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
perror("setsockopt()");
exit(1);
}
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr.s_addr);
// 绑定地址
if(bind(sd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
perror("bind()");
exit(1);
}
// 监听连接
if(listen(sd, 200) < 0) {
perror("listen");
exit(1);
}
int i;
for(i = 0;i < PROCNUM; i++) {
pid = fork();
if(pid < 0) {
perror("fork()");
exit(1);
}
if(pid == 0) { // 子进程
server_loop(sd);
exit(0);
}
}
for(i = 0;i < PROCNUM; i++) {
wait(NULL);
}
close(sd);
exit(0);
}
执行结果:
[root@zoukangche pool_static]# ./server
[2828]Client:127.0.0.1:59268
[2829]Client:127.0.0.1:59270
[2830]Client:127.0.0.1:59272
[2831]Client:127.0.0.1:59276
[2828]Client:127.0.0.1:59278
[2829]Client:127.0.0.1:59280
[2830]Client:127.0.0.1:59282
[2831]Client:127.0.0.1:59284
[2828]Client:127.0.0.1:59286
[2829]Client:127.0.0.1:59290
[2830]Client:127.0.0.1:59292
⑤ 动态进程池套接字实现
描述:进程池主进程最大可fork出20个进程来接收连接和处理任务,当一些进程完成任务后,阻塞等待连接,成为空闲进程,此时主进程需要控制空闲进程(一直阻塞等待连接)的数量,最大可空闲10个,否则多余的空闲进程由主进程杀死,减少服务器端的资源消耗,最小可空闲5个,小于5个时由主线程fork出,这一目的是提高服务器的并发性,在有多个客户端突发连接时能够得到有效处理。
主进程任务:初始化进程池,并控制进程池中进程的数量。
子进程任务:不断接收客户端连接,处理任务。
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <time.h>
#include <errno.h>
#include <sys/mman.h>
#include <signal.h>
#include <unistd.h>
#include "proto.h"
#define MINSPARESERVER 5 // 最小可空闲进程
#define MAXSPARESERVER 10 // 最大可空闲进程
#define MAXCLIENT 20 // 进程资源总量
#define SIG_NOTIFY SIGUSR2 // 自定义的信号
#define IPSTRSIZE 40
#define LINEBUFSIZE 1024
// 进程状态的枚举
enum {
STATE_IDLE = 0, // 进程空闲
STATE_BUSY // 进程忙碌
};
// 包含进程信息的结构体
struct server_st {
pid_t pid; // 进程pid
int state; // 进程状态
// int reuse;
};
static struct server_st *serverpool; // server_st数组的首地址
static int idle_count = 0, busy_count = 0; // 计数器
static int sd; // 监听套接字
// 信号SIG_NOTIFY的处理函数
static void usr2_hanlder(int s) {
return ;
}
// 子进程的任务:等待连接,处理任务
static void server_job(int pos) {
struct sockaddr_in raddr;
socklen_t raddr_len;
char ipstr[IPSTRSIZE];
time_t stamp;
int len;
char linebuf[LINEBUFSIZE];
// 父进程pid
int ppid = getppid();
int client_sd; // 副套接字
while(1) {
// 没有连接前,状态为空闲
serverpool[pos].state = STATE_IDLE;
// 状态改变时,通知主进程:作用是打断主进程中的阻塞
kill(ppid, SIG_NOTIFY);
// 阻塞等待连接
client_sd = accept(sd, (void *)&raddr, &raddr_len);
if(client_sd < 0) {
if(errno != EINTR || errno != EAGAIN) {
perror("accept()");
exit(1);
}
}
// 完成连接,设置状态为忙碌
serverpool[pos].state = STATE_BUSY;
// 状态改变时,通知主进程
kill(ppid, SIG_NOTIFY);
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
// printf("[%d]Client:%s:%d\n", getpid(), ipstr, ntohs(raddr.sin_port));
stamp = time(NULL);
len = snprintf(linebuf, LINEBUFSIZE, FMT_STAMP, stamp);
send(client_sd, linebuf, len , 0);
sleep(5);
close(client_sd);
}
}
// 创建一个子进程
static int add_1_server(void) {
int slot;
pid_t pid;
if(idle_count + busy_count >= MAXCLIENT) { // 空闲和忙碌的进程数大于进程资源总量,则不添加
return -1;
}
// 遍历serverpool数组,查找到可用的位置slot
for(slot = 0; slot < MAXCLIENT; slot++) {
if(serverpool[slot].pid == -1) {
break;
}
}
serverpool[slot].state = STATE_IDLE; // 新增进程的状态设置为空闲
pid = fork(); // 创建子进程
if(pid < 0) {
perror("fork()");
exit(1);
}
if(pid == 0) { // 子进程
server_job(slot); // 干活
exit(1);
} else { // 父进程
serverpool[slot].pid = pid; // 父进程负责设置子进程的pid
idle_count ++; // 增加一个空闲资源
}
return 0;
}
static int del_1_server(void) {
int i;
pid_t pid;
if(idle_count == 0) { // 空闲进程数为0,则不删除
return -1;
}
// 遍历serverpool数组,查找到可释放的空闲进程
for(i = 0; i < MAXCLIENT; i++) {
// 找到一个已分配的空闲进程
if(serverpool[i].pid != -1 && serverpool[i].state == STATE_IDLE) {
// 主线程发送终止信号
kill(serverpool[i].pid, SIGTERM);
serverpool[i].pid = -1;
idle_count--;
break;
}
}
return 0;
}
// 遍历进程池
static int scan_pool(void) {
int i;
int busy = 0;
int idle = 0;
for(i = 0; i < MAXCLIENT; i++) {
if(serverpool[i].pid == -1) { // 进程不存在
continue;
}
// kill(pid, 0)用于检测pid是否存在,存在返回0,不存在返回-1
if(kill(serverpool[i].pid, 0) == -1) {
serverpool[i].pid = -1;
continue;
}
if(serverpool[i].state == STATE_IDLE) {
idle++;
} else if(serverpool[i].state == STATE_BUSY) {
busy++;
} else {
fprintf(stderr, "Unknown state.\n");
abort();
}
}
idle_count = idle;
busy_count = busy;
return 0;
}
// 主进程
int main(void) {
struct sigaction sa, osa;
struct sockaddr_in laddr;
sigset_t set, oset;
int val = 1;
int i;
pid_t pid;
// 避免子进程成为僵尸进程,详见6.2.1节
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_NOCLDWAIT;
sigaction(SIGCHLD, &sa, &osa);
// 自定义信号SIG_NOTIFY的处理
sa.sa_handler = usr2_hanlder;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIG_NOTIFY, &sa, &osa);
// 对SIG_NOTIFY信号进行屏蔽
sigemptyset(&set);
sigaddset(&set, SIG_NOTIFY);
sigprocmask(SIG_BLOCK, &set, &oset);
// 为serverpool申请20个空间
serverpool = mmap(NULL, sizeof(struct server_st) * MAXCLIENT,
PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if(serverpool == MAP_FAILED) {
perror("mmap()");
exit(1);
}
// 赋初值,表示当前没有产生(fork)进程
for(i = 0; i < MAXCLIENT; i++) {
serverpool[i].pid = -1;
}
// 创建套接字
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd < 0) {
perror("socket()");
exit(1);
}
// 设置套接字选项
if(setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) {
perror("setsockopt()");
exit(1);
}
// 本机地址配置
laddr.sin_family = AF_INET;
laddr.sin_port = htons(atoi(SERVERPORT));
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr.s_addr);
// 绑定地址
if(bind(sd, (struct sockaddr *)&laddr, sizeof(laddr)) < 0) {
perror("bind()");
exit(1);
}
// 监听连接
if(listen(sd, 200) < 0) {
perror("listen");
exit(1);
}
// 初始化进程池,使其空闲进程数等于MINSPARESERVER
for(i = 0 ;i < MINSPARESERVER; i++) {
add_1_server();
}
while(1) {
// 信号驱动:当子进程的状态发生变化时,向父进程发送信号,父进程相应作出一些动作
// 解除对SIG_NOTIFY的屏蔽,并且阻塞在这里,直到子进程调用kill,发送给主进程一个信号来打断此阻塞,使得主进程能够向下执行
sigsuspend(&oset);
// 扫描进程池
scan_pool();
// 控制进程池:多退少补
if(idle_count > MAXSPARESERVER) {
for(i = 0; i < (idle_count - MAXSPARESERVER); i++) {
del_1_server();
}
} else if(idle_count < MINSPARESERVER) {
for(i = 0; i < (MINSPARESERVER - idle_count); i++) {
add_1_server();
}
}
// 输出进程池的状态
for(i = 0; i < MAXCLIENT; i++) {
if(serverpool[i].pid == -1) { // 尚未分配的进程
putchar('x');
} else if(serverpool[i].state == STATE_IDLE) { // 空闲进程
putchar('.');
} else { // 忙碌进程
putchar('$');
}
}
putchar('\n');
}
// 恢复信号屏蔽字
sigprocmask(SIG_SETMASK, &oset, NULL);
close(sd);
exit(0);
}
代码测试:
启动服务器端:
[root@zoukangcheng pool_dynamic]# ./server
.....xxxxxxxxxxxxxxx
.....xxxxxxxxxxxxxxx
.....xxxxxxxxxxxxxxx
.....xxxxxxxxxxxxxxx
.....xxxxxxxxxxxxxxx
这里出现5行的原因为初始化5个子进程。可以看到前五个为启用的进程,且是空闲的,后15个尚未启用。
服务器运行到:fork出子进程
for(i = 0 ;i < MINSPARESERVER; i++) {
add_1_server();
}
子进程运行到:
while(1) {
serverpool[pos].state = STATE_IDLE;
kill(ppid, SIG_NOTIFY); // 设置主进程的SIG_NOTIFY的pending位为1
client_sd = accept(sd, (void *)&raddr, &raddr_len); // 阻塞等待连接
// ...
}
由于主进程对SIG_NOTIFY进行了屏蔽(mask=1),因此不会响应,但是pending位为1。
主进程创建完个子进程后,进入循环:
while(1) {
sigsuspend(&oset); // 解除对SIG_NOTIFY的屏蔽,同时阻塞
scan_pool();
// ...
}
主进程阻塞在sigsuspend,然后依次响应之前的信号(存疑)。
发送一个请求,服务器端打印的结果:
..$...xxxxxxxxxxxxxx
..$...xxxxxxxxxxxxxx
......xxxxxxxxxxxxxx
某个子进程接收到连接,改变状态为忙碌,同时发送信号给父进程。父进程响应,扫描进程池后发现空闲进程数量为(5-1=4<5),因此再次fork一个子进程(注意fork出的子进程阻塞在accept之前也要向父进程发送信号)。父进程打印第一二行,分别是对原子进程和新子进程的信号响应;5s后,原子进程完成任务,将状态修改为空闲,并向父进程发送信号,父进程再打印出第三行。此时空闲进程有6个了。
连续发送请求:
while true; do (./client 127.0.0.1 &); sleep 1; done # 每1s发送一个请求,每个请求的处理时间为5s