Linux系统编程-多路IO&套接字

目录

有限状态机

多路IO

Select IO

1.select

2.FD_SET 

3.FD_ISSET

4.FD_CLR

5.FD_ZERO

6. pselect

Poll IO

Epoll IO

1.epoll_create

2.epol_create1

3.epoll_ctl 

4.epoll_wait

5.epoll_pwait

6.readv

7.writev

内存映射

文件锁

网络套接字

1.socket

2.bind 

3.listen 

4.accept 

5.connect 

6.send 

7.recv 

8.close 

9.sendto 

10.recvfrom 

广播

多播/组播(IP_MUTICAST_IF)

端口复用

多进程TCP通信实例

proto.h

server.c

client.c

UDP多播实例

proto.h

server.c

client.c


有限状态机

        有限状态机(Finite State Machine,简称 FSM)编程是一种设计范式,它使用有限状态机的概念来设计和实现软件系统。有限状态机是一种计算模型,它由一组状态以及在这些状态之间的转移组成。每个状态都对应于系统在某一特定时间点的特定行为或条件。状态机在接收到输入或触发事件时,会根据当前状态和输入来决定转移到哪个新的状态。

有限状态机编程的关键要素包括:
        -状态(States):状态是系统可以处于的明确定义的条件或情况。每个状态都对应着系统行为的一个特定方面。
        -转移(Transitions):转移是状态之间的连接,它们定义了从一个状态到另一个状态的规则。转移通常由事件触发。
        -事件(Events):事件是导致状态转移的触发器。它们可以是外部输入、内部条件或时间延迟。
        -初始状态(Initial State):这是状态机开始时的状态。
        -终止状态(Final States):在某些状态机中,当达到某个特定状态时,状态机的执行会停止。
        -动作(Actions):与状态转移相关联的操作,可以在进入或退出某个状态时执行。

有限状态机编程的步骤通常包括:
        定义所有可能的状态。
        定义触发状态转移的事件。
        定义每个状态下可执行的动作。
        实现状态转移逻辑。


多路IO

Select IO

1.select

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);


 struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

        检查一组文件描述符,确定它们中是否有任何一个准备好了进行非阻塞读、写或有异常条件。成功时返回准备好的文件描述符的数量,失败时返回 -1。
    -int nfds:要检查的最大文件描述符加一。
    -fd_set *readfds:指向要检查读状态的文件描述符集合的指针。
    -fd_set *writefds:指向要检查写状态的文件描述符集合的指针。
    -fd_set *exceptfds:指向要检查异常条件的文件描述符集合的指针(在大多数现代系统中,这个参数被忽略)。
    -struct timeval *timeout:指定 select 等待的时间长度,如果设置为 NULL,则 select 会无限期地等待。

2.FD_SET 

void FD_SET(int fd, fd_set *set);

        将指定的文件描述符添加到集合中。

3.FD_ISSET

int FD_ISSET(int fd, fd_set *set);

        检查文件描述符是否在集合中。通常用于 select 返回后,检查哪些文件描述符已经准备好。

4.FD_CLR

void FD_CLR(int fd, fd_set *set);

        从集合中移除指定的文件描述符。

5.FD_ZERO

void FD_ZERO(fd_set *set);

        初始化一个文件描述符集合,将所有文件描述符从集合中移除。

6. pselect

int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);


struct timespec {
      time_t      tv_sec;         /* seconds */
      long        tv_nsec;        /* nanoseconds */
};

Poll IO

int poll(struct pollfd *fds, nfds_t nfds, int timeout);


struct pollfd {
     int   fd;         /* file descriptor */
     short events;     /* 16位位图宏,监视的行为 */
     short revents;    /* returned events */
};

        以文件描述符为单位组织事件.是结构体数组的起始位置, int timeout: 指定 poll 函数等待 I/O 操作变为可进行状态的最大时间(以毫秒为单位)。如果设置为 -1,则表示无限期等待。成功时,返回正数,表示至少有一个文件描述符已经准备好进行 I/O 操作。错误时,返回 -1,并设置 errno 来指示错误类型。


Epoll IO

1.epoll_create

int epoll_create(int size);

        创建一个 epoll 实例。size 参数指定了可以监听的文件描述符的数量。从 Linux 2.6.8 开始,这个参数被忽略,因为内核可以动态调整大小.成功时返回新创建的 epoll 实例的文件描述符,失败时返回 -1。

2.epol_create1

int epoll_create1(int flags);

        与epoll_create 类似,但允许通过 flags 参数指定额外的选项。目前只有 EPOLL_CLOEXEC 标志被支持,它使得 epoll 实例在执行 exec 系列函数时不会被继承。

3.epoll_ctl 

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

struct epoll_event {
    uint32_t events;  /* Epoll events */
    epoll_data_t data; /* User data variable */
};

typrdef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

        控制对 epoll 实例的操作。vent 是一个指向 epoll_event 结构的指针,指定了感兴趣的事件。成功时返回 0,失败时返回 -1。

4.epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

        等待 epoll 实例中的事件。epfd 是 epoll 实例的文件描述符,events 是一个数组,用于接收触发的事件,maxevents 是数组的大小,timeout 是等待时间(单位为毫秒),0 表示立即返回,-1 表示无限等待。成功时返回触发的事件数量,失败时返回 -1。

5.epoll_pwait

int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);

        与 epoll_wait 类似,但允许在等待期间指定一个信号掩码,以屏蔽某些信号。

6.readv

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

struct iovec {
    void *iov_base;  // 指向数据缓冲区的指针
    size_t iov_len;   // 缓冲区的长度
};

        从指定的文件描述符读取数据到多个缓冲区中,iov:指向 iovec 结构数组的指针,每个 iovec 包含一个指向缓冲区的指针和一个缓冲区长度。iovcnt:iovec 数组中的元素数量。成功时返回读取的字节数;失败时返回 -1 并设置 errno。

7.writev

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

        将多个缓冲区的数据写入到指定的文件描述符。成功时返回写入的字节数;失败时返回 -1 并设置 errno。


内存映射

void *mmap(void addr[.length], size_t length, int prot, int flags, int fd, off_t offset);

        将文件或其他对象映射到进程的地址空间中,使得文件的内容可以像访问内存一样被访问。这种方式可以提高文件访问的效率,特别是在需要频繁访问文件内容时。成功返回映射地址,失败返回MAP_FAILED

 -addr:一个指针,指定映射区域的起始地址。如果 addr 是 NULL,则系统选择映射区域的地址;否则,系统会尝试将映射区域放在 addr 指定的地址附近。


-length:映射区域的长度,单位为字节。


-prot:映射区域的保护方式,可以是以下标志的组合:
            -PROT_EXEC:允许执行映射区域的内容。
            -PROT_READ:允许读取映射区域的内容。
            -PROT_WRITE:允许写入映射区域的内容。
            -PROT_NONE:不允许任何访问。


-flags:控制映射区域的行为,可以是以下标志的组合(SHARED和PRIVATE二者必选其一):
            -MAP_SHARED:映射区域对写操作是共享的,即写入映射区域的内容会反映到文件中。
            -MAP_PRIVATE:映射区域是私有的,写入映射区域的内容不会反映到文件中,而是复制到一个新创建的文件中。
            -MAP_ANONYMOUS:创建一个匿名映射,不与任何文件关联。
            -MAP_FIXED:强制将映射区域放在 addr 指定的地址,否则失败。
            -MAP_GROWSDOWN:允许映射区域向下扩展。
            -MAP_DENYWRITE:禁止写入映射区域。
            -MAP_EXECUTABLE:允许执行映射区域的内容。
            -MAP_LOCKED:锁定映射区域,防止被交换到磁盘。
            -MAP_NORESERVE:不保留交换空间。
            -MAP_POPULATE:预读映射区域的页面。


-fd:文件描述符,指定要映射的文件。如果 flags 包含 MAP_ANONYMOUS,则此参数被忽略。


-offset:文件映射的起始偏移量,通常以字节为单位。如果 flags 有MAP_ANONYMOUS,则此参数被忽略。

 

int munmap(void addr[.length], size_t length);

        解除addr的内存映射, length是长度.成功返回0,失败返回-1或errno


文件锁

        为什么会有文件锁呢?因为文件的inode可能被多个文件描述符(file descriptors)共享。这意味着多个进程或线程可能通过不同的文件描述符访问同一个文件而这些文件描述符可能指向同一个inode。文件锁通常有两种类型:文件级锁(flock)记录级锁(lockf)。文件级锁锁定整个文件,而记录级锁则锁定文件的特定部分。如果锁的粒度不够细,可能会在不应该解锁的情况下意外解锁,假设A和B两个进程使用不同的fd,操控同一个inode的文件,A对文件加锁,B对文件解锁,在A进程看来就发生了意外解锁。

int flock(int fd, int op);

        用于对文件进行加锁或解锁操作,以实现进程间的同步


-fd:文件描述符,表示要加锁或解锁的文件。这个文件描述符必须有效,并且已经打开用于读取或写入。


-op:操作类型,定义了要执行的锁操作,可以是以下值之一:
            -LOCK_SH:共享锁(Shared lock)。如果其他进程已经持有这个文件的共享锁,当前进程可以获取共享锁,但是不能获取独占锁。
            -LOCK_EX:独占锁(Exclusive lock)。如果其他进程已经持有这个文件的任何类型的锁,当前进程不能获取独占锁。
            -LOCK_NB:非阻塞模式(Non-blocking)。这个标志可以与 LOCK_SH 或 LOCK_EX 结合使用,表示如果锁不能立即被获取,flock 将立即返回错误而不是等待。
            -LOCK_UN:解锁操作(Unlock)。释放当前进程持有的锁。

int lockf(int fd, int op, off_t len);

        是一个系统调用,用于在文件上执行加锁或解锁操作,与 flock 类似,但它提供了更细粒度的控制。lockf 允许你指定锁定文件的特定部分,而不是整个文件。


-fd:文件描述符,表示要加锁或解锁的文件。这个文件描述符必须有效,并且已经打开用于读取或写入。


-op:操作类型,定义了要执行的锁操作,可以是以下值之一:
            -F_LOCK:请求一个锁定。
            -F_TLOCK:请求一个测试并锁定。如果文件已经被锁定,调用将失败并返回错误。
            -F_ULOCK:释放一个锁定。


-len:锁定区域的长度。这个值指定了从当前文件位置开始的字节数。如果 len 是 0,锁定将从当前位置开始一直延伸到文件的末尾。


网络套接字

跨主机的传输要注意的问题
字节序
- 大端 低地址放高字节
- 小端 高地址放低字节(x86)

- 主机字节序 host
- 网络字节序 network
- _ to _ 长度()
    - htons()
    - htonl()
    - ntohs()
    - ntohl()

socket:
        一个中间层,连接网络协议与文件操作
        socket就是插座,与兴在计算机中两个从小通过socket建立起一个通道,数据在通道中传输
        socket把复杂的TCP/IP协议族隐藏了起来,对于程序元来说只要用好socket相关的函数接可以完成网络通信
        socket提供了`stream` `datagram` 两种通信机制,即流socket和数据包socket,流socket基于TCP协议,是一个有序、可靠、双向字节刘的通道,传输数据不会丢失、不会重复、顺序也不会错乱,数据包socket基于UDP协议,不需要建立和尉迟连接,可能会丢失或错乱。UDP不是一个可靠的协议,对数据的长度有限制,但是效率较高

1.socket

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

        创建一个socket套接字

2.bind 

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

        将套接字绑定到特定的地址

3.listen 

int listen(int sockfd, int backlog);

        设置监听上限并监听传入的连接请求

4.accept 

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

        接受一个连接请求,返回一个新的套接字描述符

5.connect 

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

        连接到指定的服务器地址

6.send 

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

        向套接字发送数据

7.recv 

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

        从套接字接收数据

8.close 

int close(int sockfd);

        关闭套接字

9.sendto 

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

        向一个数据报套接字发送数据到指定地址

10.recvfrom 

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

        从数据报套接字接收数据


广播

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

        设置套接字选项

-sockfd:指定要操作的套接字的文件描述符。


-level:指定选项的协议层。常见的值包括SOL_SOCKET(套接字层),或者特定的协议层,如IPPROTO_TCP(TCP层)。


-optname:指定要获取或设置的选项的名称。不同的level值有不同的选项名称。


-optval:指向一个缓冲区,用于存放获取的选项值(对于getsockopt)或存放要设置的新值(对于setsockopt)。缓冲区的大小由optlen参数指定。


-optlen:一个socklen_t类型的值,表示optval缓冲区的大小。对于getsockopt,它应该在调用前被设置为缓冲区的大小,调用后,系统会更新这个值以反映实际的选项值大小。对于setsockopt,它应该被设置为要设置的选项值的大小。

多播/组播(IP_MUTICAST_IF)

        相较广播更灵活,`224.0.0.1` 这个地址表示所有支持多播的节点默认都存在于这个组中且无法离开,往这个地址发送相当于往255.255.255.255发消息

setsockopt(sfd, IPPROTO_IP, IP_MULTICAST_IF, &moptval, sizeof(optval));

端口复用

setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &valopt, sizeof(valopt));

多进程TCP通信实例

proto.h

#ifndef PROTO_H__
#define PROTO_H__

#include <stdint.h>

#define NAMEMAX 512-8-8//(UDP推荐长度-UDP报头长度-结构体的长度)
#define FMT_STAMP "%lld\n"
#define SERVERPORT "2333"


#endif

server.c

#include <asm-generic/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdarg.h>
#include <time.h>

#include "proto.h"

#define IPSIZE 1024
#define BUFSIZE 1024
#define SERVERPORT "2333"

static void debug(char *fmt,...){
    va_list ap;
    va_start(ap,fmt);

    printf("DEBUG ");
    printf(fmt,va_arg(ap,int));

    va_end(ap);
}

static void server_job(int newsd){
    char buf[BUFSIZE];
    int pkglen = 0;

    pkglen = sprintf(buf,FMT_STAMP,(long long)time(NULL));

    if (send(newsd,buf,pkglen,0) < 0){
        perror("send()");
        exit(1);
    }
}

int main()
{
    int sfd;
    struct sockaddr_in laddr;//local addr
    struct sockaddr_in raddr;//remote addr
    char ip[IPSIZE];

    sfd = socket(AF_INET,SOCK_STREAM,0/*IPPROTO_TCP*/);
    if (sfd < 0){
        perror("socket()");
        exit(1);
    }
    
    int val = 1;
    if(setsockopt(sfd,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);//IPv4点分式转二进制数

    if(bind(sfd,(void *)&laddr,sizeof(laddr)) < 0){
        perror("bind()");
        exit(1);
    }

    if(listen(sfd,1024) < 0){//全连接数量
        perror("listen()");
        exit(1);
    }


    socklen_t raddr_len = sizeof(raddr);
    pid_t pid;

    while(1){
        int newsd;
        newsd = accept(sfd,(void *)&raddr,&raddr_len);//接收客户端连接
        if (newsd < 0){
            perror("accept()");
            exit(1);
        }
        
        pid = fork();
        if (pid < 0){
            perror("fork()");
            exit(1);
        }
        if (pid == 0){
            close(sfd);
            inet_ntop(AF_INET,&raddr.sin_addr,ip,IPSIZE);
            printf("client %s %d\n",ip,ntohs(raddr.sin_port));
            server_job(newsd);
            close(newsd);
            exit(0);
        }
        close(newsd);//父子进程必须都将打开的来自client的socket关闭,否则socket不会返回client
    }

    close(sfd);
    
    exit(0);
}

client.c

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

#include "proto.h"

#define BUFSIZE 1024

int main()
{
    int sfd;
    struct sockaddr_in raddr;//remote addr

    sfd = socket(AF_INET,SOCK_STREAM,0/*IPPROTO_TCP*/);

    raddr.sin_family = AF_INET;
    raddr.sin_port = htons(atoi(SERVERPORT));
    inet_pton(AF_INET,"127.0.0.1",&raddr.sin_addr);

    if(connect(sfd,(void *)&raddr,sizeof(raddr)) < 0){
        perror("connect()");
        exit(1);
    }

    FILE *fp;
    fp = fdopen(sfd,"r+");
    if (fp == NULL){
        perror("fopen()");
        exit(1);
    }

    long long stamp;
    if (fscanf(fp,FMT_STAMP,&stamp) < 1){
        fprintf(stderr,"Bad format\n");
    }else{
        fprintf(stdout,FMT_STAMP,stamp);
    }

    close(sfd);

    exit(0);
}

UDP多播实例

proto.h

#ifndef PROTO_H__
#define PROTO_H__

#include <stdint.h>

#define NAMEMAX 512-8-8//(UDP推荐长度-UDP报头长度-结构体的长度)
#define  MULTICASTADDR "224.2.2.2"

struct msg_st{
    uint32_t math;
    uint32_t chinese;
    char name[0];
}__attribute__((packed));//不对齐

#endif

server.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdarg.h>

#include "proto.h"

#define IPSIZE 1024
#define SERVERPORT "2333"

static void debug(char *fmt,...){
    va_list ap;
    va_start(ap,fmt);

    printf("DEBUG ");
    printf(fmt,va_arg(ap,int));

    va_end(ap);
}

int main()
{
    int sfd;
    struct sockaddr_in laddr;//local addr
    struct sockaddr_in raddr;//remote addr
    struct msg_st *rbuf;
    char ip[IPSIZE];

    int pkglen = sizeof(struct msg_st)+NAMEMAX;
    rbuf = malloc(pkglen);
    if (rbuf == NULL){
        perror("malloc()");
        exit(1);
    }

    sfd = socket(AF_INET,SOCK_DGRAM,0/*IPPROTO_UDP*/);
    if (sfd < 0){
        perror("socket()");
        exit(1);
    }

    //设置socket属性
    struct ip_mreqn mreqn;
    inet_pton(AF_INET,"0.0.0.0",&mreqn.imr_address);
    //224.0.0.1 这个地址表示所有支持多播的节点默认都存在于这个组中且无法离开
    inet_pton(AF_INET,MULTICASTADDR,&mreqn.imr_multiaddr);
    mreqn.imr_ifindex = if_nametoindex("wlp7s0");
    if (setsockopt(sfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreqn,sizeof(mreqn)) < 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);//IPv4点分式转二进制数

    if(bind(sfd,(void *)&laddr,sizeof(laddr)) < 0){
        perror("bind()");
        exit(1);
    }

    socklen_t raddr_len = sizeof(raddr);
    while(1){
        recvfrom(sfd,rbuf,pkglen,0,(void *)&raddr,&raddr_len);//报式套接字每次通信都需要知道对方是谁
        inet_ntop(AF_INET,&raddr.sin_addr,ip,IPSIZE);
        printf("%s %d\n",ip,ntohs(raddr.sin_port));
        printf("%s %d %d\n",rbuf->name,ntohl(rbuf->math),ntohl(rbuf->chinese));
        fflush(NULL);
    }

    close(sfd);
    
    exit(0);
}

client.c

#include <asm-generic/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <math.h>
#include <string.h>

#include "proto.h"

#define SERVERPORT "2333"

int main()
{
    int sfd;
    struct msg_st *sbuf;
    struct sockaddr_in raddr;//remote addr
    struct ip_mreqn mreqn;

    sfd = socket(AF_INET,SOCK_DGRAM,0/*IPPROTO_UDP*/);
    if(sfd < 0){
        perror("socket()");
        exit(1);
    }
    //设置socket的属性
    inet_pton(AF_INET,"0.0.0.0",&mreqn.imr_address);
    inet_pton(AF_INET,MULTICASTADDR,&mreqn.imr_multiaddr);
    mreqn.imr_ifindex = if_nametoindex("wlp7s0");

    if (setsockopt(sfd,IPPROTO_IP,IP_MULTICAST_IF,&mreqn,sizeof(mreqn)) < 0){
        perror("setsockopt()");
        exit(1);
    }//打开广播属性
    

    int pkglen = sizeof(struct msg_st)+strlen("Mike")+1;// 注意给'/0'留位置
    sbuf = malloc(pkglen);
    if (sbuf == NULL){
        perror("malloc()");
        exit(1);
    }
    
    char *name = "Mike";
    strcpy(sbuf->name,name);
    sbuf->math = htonl(rand()%100);//主机字节序转网络字节序
    sbuf->chinese = htonl(rand()%100);

    raddr.sin_family = AF_INET;
    raddr.sin_port = htons(atoi(SERVERPORT));
    inet_pton(AF_INET,MULTICASTADDR,&raddr.sin_addr);

    if(sendto(sfd,sbuf,pkglen,0,(void *)&raddr,sizeof(raddr)) < 0){
        perror("sendto()");
        exit(1);
    }

    puts("OK");

    close(sfd);

    exit(0);
}
  • 28
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值