飞不高 发表于 2024-8-2 09:44:24

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;
    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;

    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报头长度-结构体的长度)
#defineMULTICASTADDR "224.2.2.2"

struct msg_st{
    uint32_t math;
    uint32_t chinese;
    char name;
}__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;

    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);
}
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Linux系统编程-多路IO&套接字