基本socket函数

创建网络端点

  • 创建socket描述符
int socket (int family, int type, int protocol)

Socket地址

TCP/IP协议的socket地址

struct sockaddr_in {
        short 		    sin_family;    /*AF_INET*/
        u_short 		sin_port;      /*端口号,网络字节顺序*/
        struct n_addr 	sin_addr;      /*IP地址,网络字节顺序*/
        char    		sin_zero[8];   /*填充字节,必须为全零*/
};

struct in_addr {
  	union {
          		struct { u_char s_b1,s_b2,s_b3,s_b4; }   S_un_b;
          		struct { u_short s_w1,s_w2; }   S_un_w;
         		 u_long   S_addr;
 	 } S_un;
};

地址转换函数

  • 字符串形式地址转换为网络地址形式inet_aton(const char *cp,struct in_addr *inp);
  • 网络地址转换为字符串地址形式char* inet_ntoa(struct in_addr in);

字节顺序

主机字节顺序

  • little-endian低字节在前
  • big-endian高字节在前

网络字节顺序

采用big-endian顺序

主机字节顺序和网络字节顺序的转换

unsigned short int htons(unsigned short int hostshort)
unsigned long int htonl(unsigned long int hotlong)
unsigned short int ntohs(unsigned short int netshort)
unsigned long int ntohl(unsigned long int netlong)

连接服务器

int connect(int sockfd,struct sockaddr *servaddr,int addrlen)

绑定服务器地址和端口

int bind(int sockfd,struct sockaddr *myaddr,int addrlen); 

地址可重用

bind之前

int on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

监听端口

int listen(int sockfd,int backlog)
  • sockfd-已绑定的socket描述符
  • backlog-以完成连接,等待接受的队列长度

接收客户端连接

int accept(int sockfd,struct sockaddr *clientaddr,int addrlen);

代码示例

server

#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define MAXDATASIZE 128
#define PORT 3000
#define BACKLOG 5

int main(int argc, char **argv) {
    int sockfd, new_fd, nbytes, sin_size;
    char buf[MAXDATASIZE];
    struct sockaddr_in srvaddr, clientaddr;

    // 1.创建网络端点
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        printf("can;t create socket\n");
        exit(1);
    }

    if (argc == 2) {
        int on = 1;
        setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
        printf("reuse addr\n");
    }
    //填充地址
    bzero(&srvaddr, sizeof(srvaddr));
    srvaddr.sin_family = AF_INET;
    srvaddr.sin_port = htons(PORT);
    srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*
    if(inet_aton(argv[1],&srvaddr.sin_addr)==-1){
            printf("addr convert error\n");
            exit(1);
    }
    */
    // 2.绑定服务器地址和端口
    if (bind(sockfd, (struct sockaddr *)&srvaddr, sizeof(struct sockaddr)) ==
        -1) {
        printf("bind error\n");
        exit(1);
    }
    // 3. 监听端口
    if (listen(sockfd, BACKLOG) == -1) {
        printf("listen error\n");
        exit(1);
    }
    for (;;) {
        // 4.接受客户端连接
        sin_size = sizeof(struct sockaddr_in);
        if ((new_fd = accept(sockfd, (struct sockaddr *)&clientaddr,
                             &sin_size)) == -1) {
            printf("accept error\n");
            continue;
        }
        printf("client addr:%s %d\n", inet_ntoa(clientaddr.sin_addr),
               ntohs(clientaddr.sin_port));
        // 5.接收请求
        getchar();
        nbytes = read(new_fd, buf, MAXDATASIZE);
        buf[nbytes] = '\0';
        printf("client:%s\n", buf);

        // 6.回送响应
        sprintf(buf, "wellcome!");
        write(new_fd, buf, strlen(buf));
        //关闭socket
        close(new_fd);
    }
    close(sockfd);

    return 0;
}

client

#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define MAXDATASIZE 128
#define PORT 3000

int addr_conv(char *address, struct in_addr *inaddr);

int main(int argc, char **argv) {
    int sockfd, nbytes;
    int port = PORT;
    char buf[MAXDATASIZE];
    struct sockaddr_in srvaddr;
    if (argc != 2 && argc != 3) {
        printf(
            "usage:./client hostname|ip. Or usage:./client hostname|ip port\n");
        exit(0);
    }

    if (argc == 3) port = atoi(argv[2]);
    // 1.创建网络端点
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        printf("can;t create socket\n");
        exit(1);
    }
    //指定服务器地址(本地socket地址采用默认值)
    bzero(&srvaddr, sizeof(srvaddr));
    srvaddr.sin_family = AF_INET;
    srvaddr.sin_port = htons(port);
    /*
    if(inet_aton("127.0.0.1",&srvaddr.sin_addr)==-1){
            printf("addr convert error\n");
            exit(1);
    }
    */
    if (addr_conv(argv[1], &srvaddr.sin_addr) == -1) {
        perror(strerror(errno));
    }
    // 2.连接服务器
    if (connect(sockfd, (struct sockaddr *)&srvaddr, sizeof(struct sockaddr)) ==
        -1) {
        printf("connect error\n");
        exit(1);
    }
    // 3.发送请求
    sprintf(buf, "hello");
    write(sockfd, buf, strlen(buf));
    sprintf(buf, "hello2");
    write(sockfd, buf, strlen(buf));
    sprintf(buf, "hello3");
    write(sockfd, buf, strlen(buf));
    // 4.接收响应
    if ((nbytes = read(sockfd, buf, MAXDATASIZE)) == -1) {
        printf("read error\n");
        exit(1);
    }
    buf[nbytes] = '\0';
    printf("srv respons:%s\n", buf);
    //关闭socket
    close(sockfd);
    return 0;
}

int addr_conv(char *address, struct in_addr *inaddr) {
    struct hostent *he;
    if (inet_aton(address, inaddr) == 1) {
        printf("call inet_aton sucess.\n");
        return 0;
    }
    printf("call inet_aton fail.\n");
    he = gethostbyname(address);
    if (he != NULL) {
        printf("call gethostbyname sucess.\n");
        *inaddr = *((struct in_addr *)(he->h_addr_list[0]));
        return 0;
    }
    return -1;
}

高级socket函数

DHCP

动态主机配置协议(Dynamic Host Configuration Protocol)

分配方式

  • 自动分配
  • 动态分配
  • 人工分配

DHCP过程

域名访问

域名系统——DNS

  • 域名查找过程

域名到IP的转换函数

struct hostent* gethostbyname(const char *name)

struct hostent{
	char	h_name;	        /*主机正式名称*/
	char	**h_aliases;	/*别名列表,以NULL结束*/
	int 	h_addrtype;	    /*主机地址类型:AF_INET*/
	int 	h_length;	    /*主机地址长度:4字节32位*/
	char 	**h_addr_list;	/*主机网络地址列表,以NULL结束*/
}
#define 	h_addr 	h_addr_list[0]; //主机的第一个网络地址

示例代码

#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char **argv) {
    if (argc != 2) {
        printf("invalid args\n");
    }
    struct hostent *he;
    he = gethostbyname(argv[1]);

    if (he != NULL) {
        printf("h_name:%s\n", he->h_name);
        printf("h_length:%d\n", he->h_length);
        printf("h_addrtype:%d\n", he->h_addrtype);
        int i;
        for (i = 0; he->h_aliases[i] != NULL; i++)
            printf("h_aliases[%d]:%s\n", i + 1, he->h_aliases[i]);
        printf("first ip:%s\r\n", inet_ntoa(*((struct in_addr *)he->h_addr)));

        for (i = 0; he->h_addr_list[i] != NULL; i++)
            printf("ip%d:%s\n", i + 1,
                   inet_ntoa(*(struct in_addr *)he->h_addr_list[i]));

    } else {
        printf("gethostbyname error: %s\n", hstrerror(h_errno));
    }
    return 0;
}

IP到域名的转换函数

  • 查询IP对应的域名
struct hostent *gethostbyaddr(const char *addr, size_t len, in family);
  • 示例代码
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>

int main(int argc, char *argv[]) {
    struct in_addr addr;

    inet_aton(argv[1], &addr);

    struct hostent *he;
    he = gethostbyaddr((char *)&addr, 4, AF_INET);

    if (he != NULL) {
        printf("h_name: %s\n", he->h_name);
    } else {
        printf("gethostbyaddr error: %s", hstrerror(h_errno));
    }
    return 0;
}

高级Socket函数

recv和send

int recv(int sockfd,void* buf,int len, int flags);
int send(int sockfd,void* buf,int len,int flags);
  • flags
    • MSG_DONTROUTE不路由——主机在本地网,不需路由。多网卡时,逐个搜索
    • MSG_OBB带外数据——紧急数据
    • MSG_PEEK不从缓存区移走数据——多进程共享数据,还可以用来查看缓存区数据
    • MSG_WAITALL等待所有数据——发现文件结束符时(Crtl+D),函数也结束

shutdown关闭连接

int shutdown(int sockfd,int howto); 
  • howto = 0对后来接收到的数据返回确认后丢弃
  • howto = 1继续发送发送缓冲区未发送完的数据,然后发送FIN字段关闭写通道
  • howto = 2关闭读写通道,任何进程不能再操作这个socket
    • close的区别
      • shutdown操作连接通道,其他进程不能再使用已被关闭的通道;close操作描述符,其他进程仍然可以使用该socket描述符
      • close关闭应用程序与socket的接口,调用close之后进程不能再读写这个socket;shutdown可以只关闭一个通道,另一个通道仍然可以操作

UDP与原始Socket编程

UDP Socket编程

recvfrom:接受UDP数据包

int recvfrom(int sockfd, void *buf, int len, unsigned char flags,
             struct socketaddr *from, socklen_t *addrlen);

sendto:发送UDP数据包

int sendto(int sockfd,const void *buf,int len,unsigned char flags,
           struct socketaddr *to,int  tolen); 

UDP服务器

  • 服务器不接受客户端连接,只需监听端口
  • 循环服务器,可以交替处理各个客户端数据包
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>

#include <iostream>

using namespace std;

int main(int argc, char **argv) {
    if (argc != 2) {
        cout << "argument invalid" << endl;
        return 1;
    }
    short port = atoi(argv[1]);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        cout << "create socket error" << endl;
        return 1;
    }
    sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    //绑定服务器地址
    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        cout << "bind error" << endl;
        return 1;
    }
    for (;;) {
        char buf[32];
        sockaddr_in client_addr;
        socklen_t addr_len;
        //接收客户端数据包
        int n = recvfrom(sockfd, buf, 16, 0, (struct sockaddr *)&client_addr,
                         &addr_len);
        if (n >= 0) {
            buf[n] = 0;
            cout << "recv:" << buf << endl;
            struct timeval tv;
            gettimeofday(&tv, NULL);
            sprintf(buf, "%d %d", (int)tv.tv_sec, (int)tv.tv_usec);
            //利用recvfron中得到的地址回送数据包
            sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&client_addr,
                   sizeof(client_addr));
        }
    }
    close(sockfd);
    return 0;
}

UDP客户端

  • 客户端不用建立连接,第一次调用sendto函数时,UDP协议为这个UDP socket选择一个端口号,以后的发送和接受操作均使用这个端口号
  • 客户端可以接收来自任何主机的数据报
  • 客户端可能永远阻塞(服务器主机崩溃)
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#include <iostream>

using namespace std;

int main(int argc, char **argv) {
    if (argc < 2) {
        cout << "argument invalid" << endl;
        return 1;
    }
    short port = atoi(argv[1]);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        cout << "create socket error" << endl;
        return 1;
    }

    sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (argc == 3 && strcmp(argv[2], "-c") == 0) {
        //记录服务器地址
        connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
    }
    for (int i = 0; i < 10; i++) {
        char buf[16];
        sprintf(buf, "%d hello", getpid());
        cout << "send:" << buf << endl;
        int n;
        if (argc == 3 && strcmp(argv[2], "-c") == 0) {
            //发送时不需要服务器地址
            n = sendto(sockfd, buf, strlen(buf), 0, NULL, 0);
        } else {
            //发送时需要服务器地址
            n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&addr,
                       sizeof(addr));
        }
        n = recvfrom(sockfd, buf, 16, 0, NULL, NULL);
        if (n >= 0) {
            buf[n] = 0;
            cout << "recv:" << buf << endl;
        }
        sleep(1);
    }
    close(sockfd);
    return 0;
}

有连接的UDP Socket

  • 在UDP Socket上调用connect函数,但不会产生3次握手过程,只记录连接另一方的IP和端口,connect函数马上返回

使用UDP Socket的说明

  • UDP协议不保证数据包可靠到达(超时和重发机制
  • UDP协议不保证数据报顺序到达(数据报序列号区分)
  • UDP协议没有流控

UDP广播

server
#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    int sockfd;
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        exit(-1);
    }
    int on = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST | SO_REUSEADDR, &on,
               sizeof(int));
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("255.255.255.255");
    addr.sin_port = htons(8080);

    char msg[] = "Broadcast Message: Hello!";
    int n;
    if ((n = sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr *)&addr,
                    sizeof(addr))) < 0) {
        exit(-1);
    }
    printf("msg=%s, msgLen=%ld, sendBytes=%d\n", msg, strlen(msg), n);
    close(sockfd);
    return 0;
}
client
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    int sockfd;
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        exit(-1);
    }

    int on = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0) {
        exit(-1);
    }

    int n;
    char buf[256];
    socklen_t addr_len = sizeof(struct sockaddr_in);
    if ((n = recvfrom(sockfd, buf, 256, 0, (struct sockaddr *)&addr,
                      &addr_len)) < 0) {
        exit(-1);
    }
    buf[n] = '\0';
    printf("Received: %s\n", buf);
    close(sockfd);
    return 0;
}

原始Socket编程

概述

  • TCP、UDP Socket对TCP协议和UDP协议做了封装来简化编程接口,但失去了对IP数据包操作的灵活性
  • 原始Socket直接针对IP数据包编程,具有更强的灵活性
  • 可以编写基于IP协议的高层协议

发送数据包

  • 没有调用connect函数绑定对方地址时必须用sendtosendmsg发送数据包;调用connect绑定对方IP地址后,可以使用writesend发送数据包

接收数据包

  • UDP包和TCP包
  • 大多数ICMP包的拷贝将传递给原始socket
  • 其他类型的数据包的拷贝传递给匹配的socket
  • 内核不能识别的IP数据包将传送给匹配的原始socket

Linux进程与信号机制

概述

  • linux进程是系统进行资源分配和调度的基本单位。
  • 进程的状态:
    1. 新建
    2. 运行
    3. 阻塞
    4. 就绪
    5. 完成
  • 按继承关系分类
    1. 父、子、孙进程
    2. 兄弟进程
    3. 孤儿进程

创建进程

  • pid_t fork(void);
    • 功能:创建新的进程,调用者成为父进程,产生的新进程成为子进程
    • 返回值:
      • > 0, 子进程的id,只在父进程中返回
      • -1, 调用失败
      • =0, 只在子进程中返回
    • 头文件:#include <sys/types.h> #include <unistd.h>
  • fork的原理
    • 两次返回
      • 调用fork的进程(父进程)返回正整数(子进程ID)
      • 在新创建的进程(子进程)中返回0,表示是子进程
    • 在调用fork时发生了什么
      • 系统创建新进程,并为该进程准备数据段、堆栈段和代码段
      • 代码段使用和父进程相同的代码段
      • 父进程的数据段和堆栈段被复制(copy on write, 写入时复制)给子进程
    • 子进程和父进程共享的内容
      • 代码段
      • 用户标识符
      • 环境变量
      • 打开的文件描述符(Socket描述符)
      • 根目录
      • 当前工作目录
      • 创建文件的模式
      • ⚠️ 数据段和堆栈段通过复制方式共享,因此子进程或父进程修改了变量值后不会影响另一个进程,即使是全局变量。
    • 父子进程执行顺序随机
  • 执行另一个程序
    • int execve(const char *path,char * const argv[],char *envp);只有execve是真正的系统调用
    • int execl(const char *path, const char * argv,…);
  • 注意:
    1. fork()exec()这两个函数,前者用于并行执行,父、子进程执行相同正文中的不同部分;后者用于调用其他进程,进程执行新的正文。
    2. fork()以后,父、子进程共享代码段,并只重新创建数据有改变的页(段页式管理)
    3. exec()以后,建立新的代码段,用被调用程序的内容填充。
    4. 前者的子进程执行后续的公共代码,后者的子进程不执行后续的公共代码
#include <iostream>
#include <unistd.h>
using namespace std;

int main() {
    pid_t pid;

    if ((pid = fork()) == 0) {
        // 子进程
        cout << "Son Process" << endl;
        exit(0);
    } else if (pid > 0) {
        // 父进程
        cout << "Father Process" << endl;
        exit(0);
    } else {
        cout << "Error" << endl;
        exit(1);
    }
    return 0;
}

创建守护进程

  • fork()子进程,父进程退出
  • 子进程建立新会话setsid()
  • 改变当前工作目录chdir(不是必须)
  • 重设文件掩码(不是必须)
    • 子进程会继承父进程的掩码
    • 增加子进程程序的灵活性
    • umask(0);
  • 关键文件描述符(不是必须)
    • close(0), close(1), close(2)
    • 释放资源
  • 执行核心工作
#include <cstdio>
#include <cstdlib>
#include <signal.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

using namespace std;
int main(int argc, char const *argv[]) {
    
    // 1. 调用fork,父进程退出,子进程继续运行
    pid_t pid = fork();
    if (pid < 0) {
        exit(1);
    } else if (pid > 0) {
        exit(0);
    }
    // 2. 调用setsid变成会长
    // 会长就是一个守护进程
    setsid();

    // 3. 忽略SIGHUP信号
    signal(SIGHUP, SIG_IGN);
    
    // 再次fork,父进程(session的头进程)退出
    if ((pid = fork()) > 0) {
        exit(0);
    } else if (pid < 0) {
        exit(1);
    }


    // 4. chdir 改变当前工作目录
    chdir("/tmp");
    
    // 5. 重设文件掩码
    umask(0);

    // 6. 关闭所有打开的文件描述符
    for (int i = 0; i < NOFILE; i++) {
        close(i);
    }

    // 7. 为标准输入(0),标准输出(1)、标准错误输出(2)打开新的文件描述符
    int fd_rd = open("/dev/null", O_RDONLY);
    int fd_wr = open("/root/deamon.log", O_WRONLY);
    dup(fd_rd);
    dup(fd_wr);

    // 8. 处理SICHLD,避免守护进程的子进程称为僵尸进程
    signal(SIGCHLD, SIG_IGN);

    // 让子进程一直活着
    while (true) {
    }
    return 0;
}

信号机制

信号分类

  • 常用信号:
    • SIGALARM——计时器到时
    • SIGCHLD——子进程停止时通知父进程
    • SIGKILL——终止进程
    • SIGSTOP——停止进程(暂停)
    • SIGINT——中断字符
  • 可靠信号和非可靠信号
  • 实时信号和非实时信号

发送信号

  • int kill(pid_t pid, int sig)
  • int raise(int sig)向进程自身发送信号
  • unsigned int alarm(unsigned int seconds)
  • void abort()
  • int sigqueue(pid_t pid, int sig, const union sigval val)
  • kill发送信号
  • 用特定的键盘字符产生信号
    • CTRL+C产生SIGINT
    • CTRL+BACKSPACE产生SIGQUIT

接收信号

  • int sigcation(int signum, const struct sigaction *act, struct sigaction *oldact);
  • sigaction结构
struct sigaction {                  
	void (*sa_handler)(int);  			            // 函数指针
	void (*sa_sigaction)(int, siginfo_t *, void *); //函数指针
	sigset_t sa_mask; 		                        // 屏蔽的信号集
	int sa_flags;			                        // 标志,SA_SIGINFO
	void (*sa_restorer)(void); 	                    // 已废弃
}

示例

INT信号处理

#include <iostream>
#include <csignal>
#include <unistd.h>

using namespace std;

void signalHandler(int signum) {
    cout << "Catched signal: " << signum << endl;
    // exit(signum);
}

int main() {
    // 注册信号SIGNAL和信号处理程序
    signal(SIGINT, signalHandler);
    
    while (true) {
        cout << "Going to sleep..." << endl;
        sleep(1);
    }
    return 0;
}

进程终止

exit()

处理子进程死亡

  • 僵尸进程(zombie)
    • 子进程终止时如果父进程存在且未处理SIGCHLD信号则子进程变为僵尸进程
    • 僵尸进程占据系统进程表项
    • 对比孤儿进程
      • 子进程终止,父进程并没有调用 wait/waitpid 获取子进程的终止状态,且父进程还没有结束(子进程没有被 init 收养),那么当子进程结束后,它的进程描述符仍然保存在系统中,这就成了僵尸进程。
      • 子进程还没有结束,但是父进程结束了,这个时候子进程失去其唯一的父进程,成为了孤儿进程。
  • 清理僵尸进程的方法1
    • 忽略SIGCHLD信号(使用信号处理函数(SIG_IGN)
    • 忽略SIGCHLD信号时,系统将清除子进程的进程表项,这种方法依赖于Linux版本的实现
  • 终端操作
    • top,查看动态进程状态
    • `ps -A -ostat, ppid, pid, cmd | grep -e ‘^[Zz]’,查看僵尸进程
    • kill -HUP xxxx, 清除僵尸
  • 清除僵尸进程的方法2
    • 调用waitwaitpid等待子进程
      • pid_t wait(int *status);等待任意子进程终止,没有子进程终止时阻塞,如果没有子进程返回-1
      • pid_t waitpid(int pid, int *status, int option)
      • 此方法没有兼容性问题
  • 清除僵尸进程的方法3
    • 捕获SIGCHLD信号
  • 清除僵尸进程的方法4
    • 调用fork()两次,使得子进程成为孤儿进程,由init管理
      • 这种方法第一次调用fork产生的子进程可能成为僵尸进程
      • 这种方法第二次调用fork产生的子进程由init处理子进程退出,不会成为僵尸进程

      通俗点讲,就是爷爷第一次 fork 生一个老爸,老爸出生后立刻 fork 生下儿子,这个时候老爸的任务就结束了,可以死掉了 (exit),这个时候儿子被强大的 init 收养,爷爷爱干啥干啥,从而儿子永远不会成为僵尸进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid;

    if ((pid = fork()) == 0) {
        pid = fork();
        if (pid > 0) {
            // 父亲生下儿子直接退出,儿子会被收养
            exit(0);
        }

        sleep(0.5);
        printf("I'm son after second fork.\n");
        printf("my parent's pid: %d\n", getppid());

        exit(0);
    }

    // 爷爷生下父亲后直接等待为其收尸
    waitpid(pid, NULL, 0);

    // 爷爷尽情快活

    return 0;
}

进程同步

当fork调用成功后,父子进程各做各的事情,但当父进程的工作告一段落,需要用到子进程的结果时,它就停下来调用wait,一直等到子进程运行结束,然后利用子进程的结果继续执行。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pc, pr;
    int status;

    pc = fork();
    if (pc < 0)
        printf("Error occured on forking.\n");
    else if (pc == 0) {
        /* 子进程的工作 */
        printf("son\n");
        exit(0);
    } else {
        /* 父进程的工作 */
        printf("father\n");
        pr = wait(&status);
        /* 利用子进程的结果 */
    }
    return 0;
}

Linux进程间通信(IPC)

管道

  • 单向通信,实现双向通信需创建两个管道
  • 只适用于父子间进程通信

使用管道

  1. pipe创建两个管道pipe1pipe2
  2. pipe[0]读,pipe[1]
  3. fork()创建子进程
  4. 父进程用pipe1写数据(关闭pipe1读端口),pipe2读数据(关闭pipe2写端口)
  5. 子进程用pipe1读数据(关闭pipe1写端口),pipe2写数据(关闭pipe2读端口)

示例

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <iostream>

using namespace std;

int main(int argc, char **argv) {
    int pipe1[2], pipe2[2];
    char pstr[] = "parent data";
    char cstr[] = "child data";
    char buf[100];

    if (pipe(pipe1) < 0 || pipe(pipe2) < 0) cout << "pipe error" << endl;
    pid_t pid = fork();
    if (pid > 0) {
        // 父进程,用管道1写数据,管道2读数据
        close(pipe1[0]);  //关闭pipe1读端口
        close(pipe2[1]);  //关闭pipe2写端口
        write(pipe1[1], pstr, sizeof(pstr));
        if (read(pipe2[0], buf, 100) > 0)
            cout << "parent received:" << buf << endl;
    } else if (pid == 0) {
        // 子进程用管道1读数据,管道2写数据
        close(pipe1[1]);  //关闭pipe1写端口
        close(pipe2[0]);  //关闭pipe2读端口
        if (read(pipe1[0], buf, 100) > 0)
            cout << "child received:" << buf << endl;
        write(pipe2[1], cstr, sizeof(cstr));
        exit(0);
    } else
        cout << "fork error" << endl;
    return 0;
}

命名管道

特点

  • 与一个路径名相关联,以文件形式存在于文件系统中
  • 该文件名所对应的文件没有数据只是为了便于其他进程引用
  • 可以在兄弟进程通信

创建

int mkfifo(char *pathname, mode_t mode);

使用

  • 写进程mkfifo创建命名管道
  • 写进程open写阻塞方式打开管道
  • 读进程open读阻塞方式打开管道
  • 写进程调用write写,读进程read读出数据

示例

fifo_server.cpp

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <iostream>

using namespace std;

#define FIFO_NAME "/home/fffzlfk/fifo_test"

int main(int argc, char **argv) {
    char pstr[] = "server data";

    if (mkfifo(FIFO_NAME, O_CREAT | O_EXCL) < 0 && (errno != EEXIST))
        cout << "create fifo error" << endl;

    int fd;
    if (argc == 2 && strcmp(argv[1], "-b") == 0)
        fd = open(FIFO_NAME, O_WRONLY, 0);
    else
        fd = open(FIFO_NAME, O_WRONLY | O_NONBLOCK, 0);
    if (fd != -1)
        cout << "open success" << endl;
    else {
        perror("open fail");
        return 0;
    }
    int write_num = write(fd, pstr, sizeof(pstr));
    if (write_num == -1) {
        if (errno = EAGAIN) cout << "write fifo error,try later:" << endl;
    } else
        cout << "real write num is:" << write_num << endl;
    return 0;
}

fifo_client.cpp

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <iostream>

using namespace std;

#define FIFO_NAME "/home/fffzlfk/fifo_test"

int main(int argc, char **argv) {
    char buf[1024];
    int fd;
    if (argc == 2 && strcmp(argv[1], "-b") == 0)
        fd = open(FIFO_NAME, O_RDONLY, 0);
    else
        fd = open(FIFO_NAME, O_RDONLY | O_NONBLOCK, 0);
    if (fd != -1)
        cout << "open success" << endl;
    else {
        perror("open fail");
        return 0;
    }
    int read_num = 20;
    memset(buf, 0, sizeof(buf));
    read_num = read(fd, buf, 1024);
    if (read_num == -1) {
        if (errno == EAGAIN) cout << "no data,try later:" << endl;
    } else {
        cout << "real read bytes:" << read_num << endl;
        cout << "read data:" << buf << endl;
    }
    //删除管道文件
    // unlink(FIFO_NAME);
    return 0;
}

Unix域Socket

  • 不是真正的网络协议
  • 提供同一台机器的的进程间通信
  • 是双向通道
  • 分为命名和非命名两种

命名Unix域Socket

  • 特点
    • 服务器可以接收多个客户端连接请求
    • 客户端调用函数connect服务器连接
      • connect使用的socket应该是已打开的UNIX域socket
      • 客户端必须拥有打开socket地址所指文件权限
      • 监听socket的连接队列满时connect立刻返回错误

非命名Unix域Socket

  • 特点
    • 无名的
    • 全双工
    • 不需要连接
    • 父子进程间通信使用socketpair

I/O模型

阻塞I/O模型

  • 产生阻塞的原因——时间片调度算法
  • 好处——阻塞进程不占用CPU时间
  • 产生阻塞的函数——读、写、建立连接、接受连接
    • 读:readreadvrecvrecvfromrecvmsg
    • 写:writewritevsendsendtosendmsg
    • 建立连接:connect
    • 接受连接:accept
  • 超时控制
    • 调用alarm函数设置超时
    • 设置socket选项——设置SO_RCVTIMEOSO_SNDTIMEO选项

非阻塞I/O模型

  • 设置Socket为非阻塞方式
    • 函数fcntl
      int flags;
      flag = fcntl(sockfd,F_GETFL,0);
      fcntl(sockfd,F_SETFL,flag|O_NONBLOCK);
      
    • 函数ioctl
      int on=1;
      ioctl(sockfd,FIONBIO,&on);
      
  • 检查操作是否可以完成的方式(轮询)

输入输出多路复用I/O模型

int select(int maxfd,  struct fd_set* rdset,  struct fd_set* wrset, 
		   struct fd_set* exset,  struct timeval* timeout);

void FD_SET(int fd,fd_set *fdset)   //将fd加入到fdset
void FD_CLR(int fd,fd_set *fdset)   //将fd从fdset里面清除
void FD_ZERO(fd_set *fdset)         //从fdset中清除所有的文件描述符
int FD_ISSET(int fd,fd_set *fdset) //判断fd是否在fdset集合中

select()可以设置超时,使长期没有文件描述符就绪时,进程可以跳出阻塞状态。select()的第一个参数 maxfd 是集合中最大的文件描述符加1,如:一个包含3个套接字描述符的集合{12,23,30},那么 maxfd 就应该是30+1=31。

在我们调用select( )时,进程会一直阻塞到以下的一种情况发生:

  • 有文件可以读,包括出现错误;
  • 有文件可以写,包括出现错误;
  • 超时所设置的时间到;
  • 被信号中断。

信号驱动I/O模型

  1. 设置SIGIO信号处理函数
  2. 设置socket描述符所有者
  3. 允许这个socket进行信号驱动I/O
void sigio_handler(int signo) {
    ...
}

int main() {
    int sockfd;
    int on = 1;
    ...
    signal(SIGIO, sigio_handler);
    fcntl(sockfd, F_SETOWN, getpid()); // 设置套接字所有者为当前进程
    ioctl(sockfd, FIOASYNC, &on);      // 启动信号驱动模式
    ...
}

服务器模型

网络服务器分类

  • 循环服务器:同一时刻只能处理一个客户端请求
  • 并发服务器:同一时刻可以处理多个客户端请求
  • UDP和TCP服务器模型
    • UDP服务器通常采用循环服务器模型
    • TCP服务器通常采用并发服务器模型

循环服务器模型

UDP循环服务器模型

TCP循环服务器模型

并发服务器模型

UCP并发服务器模型

TCP并发服务器模型

  • 一个子进程对应一个客户端
    • 创建子进程开销大,适合长时间客户请求(如FTP)
    • 客户端数量大、请求时间短会大大降低效率(如HTTP)
  • 延迟创建子进程
    • 处理短请求以循环方式完成
    • 处理时间长的请求以并发方式完成
  • 预创建子进程
    • 数量固定
      • 所有进程调用accept,无连接时将睡眠
      • 有连接到来时所有进程被唤醒
      • 某一个进程接受连接后,其余连接继续睡眠
    • 动态子进程数
      • 父进程与子进程通过管道通信
      • 子进程接收连接时给父进程发1,关闭时发0
      • 父进程收到1时,检查空闲子进程数是否小于上限,小于则创建新的子进程
      • 父进程收到0时,检查空闲子进程数是否大于上限,大于则终止一些子进程
  • 多路复用I/O
    • select函数检查侦听socket是否有连接到达、已连接socket是否有数据到达、已连接socket是否可以写数据
    • 在测试是否可读的描述符集合rdset中同时包含侦听socket和已连接socket,在测试是否可写描述符集合中包含已连接socket,就可以实现多路复用

Linux epoll

  • 一种多路复用模型
  • 对比select
    • select

      int n = select(maxfd+1, &rds, NULL, NULL, 100);
      
      if (n > 0) {
          for(int i = 0; i < fdset_size; i++) {
              if (FD_ISSET(allFD[i],&rds)) {
                  handleEvent(allFD[i]);
              }
          }
      }
      
    • epoll

      int n = epoll_wait(epfd, events, 10, 100);
      
      for(int i = 0; i < n;i++) {
          handleEvent(events[n]);
      }
      

带外数据(OOB)

带外数据(Out Of Band):传输层使用带外数据发送重要数据

紧急状态

  • 发送方TCP协议保证紧急状态能够立即发送
  • 接收方通过信号SIGURGselect函数得知紧急状态

TCP带外数据发送

  • TCP只支持1字节带外数据
  • TCP使用URG标志位和紧急指针指明带外数据:紧急指针=带外数据位置+1

发送TCP带外数据的函数

send函数和标志MSG_OOB

一个进程已经往TCP连接的发送缓冲区写入了N个字节的普通数据,然后该进程又向这个连接写入了3字节的带外数据“abc”。此时,待发送的TCP报文段头部将被设置为URG标志,并将紧急指针设置为指向带外数据的下一字节。

  • 发送单个字节,这个字节被认为是带外数据send(sockfd, "A", 1, MSG_OOB);
  • 发送多个字节,只有最后一个字节被认为是带外数据send(sockfd, "ABC", 3, MSG_OOB);,其他数据被当作普通数据。

TCP接收带外数据的过程

TCP接收端在收到紧急指针标志时检查紧急指针,然后根据紧急指针的位置确定带外数据的位置,并将它读入一个特殊的缓存中(1字节),称之为带外缓存。

  • 未设置SO_OOBINLINE选项时新到来的带外数据将覆盖未处理的带外数据。
  • 设置了SO_OOBINLINE选项时新到来的带外数据不会覆盖未处理的带外数据,但未处理的带外数据将会变成普通数据。

代码

复习

  • 一台工作于内外网模式的主机,具有一个外网地址IP_OUTTER和一个内网地址192.168.1,内网的掩码为192.168.1.125。该主机可将外网用户的请求广播给内网用户。请设计和实现该主机程序。

    • 问题分析和方案设计
      • TCP协议具有稳定可靠的特性,本题中外网通信属于单播,使用TCP协议能够具有良好性能,因此外网通信使用TCP套接字。
      • 内网涉及到广播,适合使用UDP协议进行工作,因此内网采用UDP套接字工作。
      • 主机采用单进程、阻塞式工作。
    • 编程
      #include <arpa/inet.h>
      #include <cstring>
      #include <iostream>
      #include <netinet/in.h>
      #include <sys/socket.h>
      #include <unistd.h>
      
      using namespace std;
      
      const int port = 8080;
      
      int main() {
          int sockt, connfd, socku;
          struct sockaddr_in addr, addrX;
          char buf[2048];
      
          if ((sockt = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
              exit(-1);
          }
      
          bzero(&addr, sizeof(addr));
          addr.sin_family = AF_INET;
          addr.sin_port = htons(port);
          addr.sin_addr.s_addr = htonl(INADDR_ANY);
      
          if ((bind(sockt, (sockaddr *)&addr, sizeof(addr))) < 0) {
              exit(-1);
          }
      
          if (listen(sockt, 5) == -1) {
              exit(-1);
          }
      
          struct sockaddr_in cli_addr;
          socklen_t sin_size = sizeof(struct sockaddr_in);
          for (;;) {
              connfd = accept(sockt, (sockaddr *)&cli_addr, &sin_size);
              if (connfd < 0) {
                  exit(-1);
              }
              int n = read(connfd, buf, 2048);
              if (n <= 0) {
                  exit(-1);
              }
              if ((socku = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
                  exit(-1);
              }
              bzero(&addrX, sizeof(addr));
              addrX.sin_family = AF_INET;
              addrX.sin_port = htons(port);
              addrX.sin_addr.s_addr = inet_addr("192.168.1.1");
              n = sendto(socku, buf, n, 0, (sockaddr *)&addrX, sizeof(addrX));
              close(socku);
              close(sockt);
          }
          return 0;
      }
      
  • 有一台服务器,可以为多个客户机同时提供两个整数的乘法和除法运算,请设计和实现该服务器。提示:需考虑应用层协议、僵尸进程的清除等问题。

    • 问题分析和方案设计
      • 本题需要同时为多个客户机服务,应采用并发方式;
      • 对于整数、要考虑字节顺序转换;
      • 除法要考虑除0问题;
      • 设计服务器接收数据格式如下:
        • int mType (运算类型:0-乘、1-除)
        • n1(整数1),n2(整数2)
      • 服务器发送数据格式如下:
        • int mRet(0-正确、1-错误)
        • n(运算结果)
      • 工作于TCP方式。
    • 编程
      void sigchild_handler(int sig) {
          wait(NULL);
      }
      
      main() {
          int sock, connfd;
          struct sockaddr_in addr;
          struct sigaction sigact;
          short mRet, mType;
          int n, n1, n2;
          char buf[1024];
      
          sigact.sa_handler = sigchild_handler;
          sigact.sa_mask = 0;
          sigact.sa_flags = 0;
          sigaction(SIGCHILD, &sigact, NULL);
      
          if ((sock = socket(AF_INET, SOCK_STREAM, 0))< 0) {
              exit(-1);
          }
          SET_ADDR_PORT;
      
          if (bind() < 0) {
              exit(-1);
          }
      
          if (listen(sock, 5) < 0) {
              exit(-1);
          }
      
          for (;;) {
              connfd = accept();
              if (connfd < 0) {
                  exit(-1);
              }
      
              if (fork() == 0) {
                  close(sock);
                  int n = read(conndf, buf, 2048);
                  if (n <= 0) {
                      exit(-1);
                  }
                  mRet = 0;
                  ntoh(CharToInt);
                  if (mType == 0) {
                      n = hton(n1*n2);
                  } else if (mType == 1 && n2 != 0) {
                      n = hton(n1/n2);
                  } else {
                      mRet = hton(1);
                      n = 0;
                  }
                  IntToChar;
                  write(connfd, buf, 2*sizeof(int));
                  close(connfd);
                  exit(0);
              }
              exit(0);
          }
          close(sock);
      }