UNIX网络编程(UNP) 第三章学习笔记

首先我们需要了解套接字地址结构到底长啥样,我们可以在<netinet/in.h>中找到下面的结构

   /*
     * Internet address (a structure for historical reasons)
     */
    struct in_addr {
    	in_addr_t s_addr; // 32位的IPV4地址,一般实现方式是uint32_t
    };
    /*
     * Socket address, internet style.
     */
    struct sockaddr_in {
    	__uint8_t       sin_len;    //非POSIX规范要求,表示的是套接字地址结构的长度
    	sa_family_t     sin_family; //表示套接字地址结构的地址族,如果有sin_len,一般为8位无符号整数,否则就是16位无符号整数
    	in_port_t       sin_port;   //TCP或者UDP接口,一般是uint16_t
    	struct  in_addr sin_addr;   //地址,具体看上面
    	char            sin_zero[8];//非强制要求,但是几乎所有的实现都增加了这个
    };

我们需要注意的是,POSIX中规定必须包含的仅仅是sin_family,sin_port和sin_addr。 sin_len是BSD为了增加对OSI协议的支持增加的,而且一般来说不需要我们设置;sin_zero则一般默认是0

我们需要记住的是,在结构中,端口号(sin_port)和sin_addr都是按照网络字节序存储的(一般就是大端法)

当然可能我们也会奇怪为什么sin_addr是一个只包含in_addr_t的in_addr结构(而不是直接保存in_addr_t),这是因为历史沿革,最早的时候in_addr包括了若干个union来实现ABC地址,但是后来ABC地址不那么重要之后,就被去掉只剩下s_addr

当我们定义了结构之后,当然要考虑怎么向套接字函数如bind之类传递,一般而言,我们会传入套接字结构的引用(指针),考虑到我们面对的套接字结构的多样性,现代的话我们也许会考虑使用void *,遗憾的是,在当时设立标准的时候,却还没有void *这个特性,他们的解决方法是采用一个通用的套接字地址结构,准确来说,是在<sys/socket.h>上定义了如下的套接字结构

 /*
     * [XSI] Structure used by kernel to store most addresses.
     */
    struct sockaddr {
    	__uint8_t       sa_len;         /* total length */
    	sa_family_t     sa_family;      /* [XSI] address family */
    	char            sa_data[14];    /* [XSI] addr value (actually larger) */
    };

于是当我们需要转换的时候,我们可以用类似下面的代码

struct sockaddr_in serv;
//中间包括对地址的填充
bind(sockfd,(struct sockaddr*)&serv,sizeof(serv));

我们可以认为sockaddr结构体的作用就是让我们完成这么个强制类型转换

值-结果参数

如果说我们观察bind函数定义和accept函数定义的话(int accept(int, struct sockaddr * __restrict, socklen_t * __restrict)),我们会发现有一个些微的区别,就是bind第三个参数size是通过传值实现的,而accept则需要我们传入一个指针,这是为什么呢?

其实道理很简单,当我们使用bind函数的时候,我们是要将进程数据向内和空间发送(我们请求bind),于是我们需要提供值给内核空间让它知道复制在什么时候结束,同样的函数还包括connect和sendto

但是当使用到accept等的时候,事情就会复杂点,一方面,当调用函数的时候,进程需要告知内核空间你该读多少长度(就像是传值一样),而当函数结束的时候,内核空间又会写入长度来告知进程该读取多少(这个时候反映的是结果),这种参数我们可以称之为“值-结果参数”,因为一个参数同时负责调用时传入值和返回时传入结果的责任,除了accept之外,有这种“值-结果参数”的还有recvfrom(),getsockname()和getpeername()

字节排序函数

我们需要注意的是,整数的存储到底是最高有效位在前还是在后其实是没有固定的,如果最高有效位在前,我们可以称之为大端法,反之则是小端法,但是两种字节排序的方法都有系统再用,显然如果一个小端法的主机向大端法按照顺序发送数据,会得到不一致的结果

庆幸的是,网络协议规定了特定的网络字节序(一般是大端法),考虑到规定了套接字地址结构中端口号和地址都必须按照网络字节序存储,因此我们有必要去知道一些实现函数

具体来说,我们可以利用htonl,htons,ntohl,ntohs四个函数实现

这四个函数其实挺好记忆的,htonl->host(h) -to - network(n)-long(l),也就是说从主机到网络就是htonl或者htons,l和s分别是long(32位,四字节,适用于地址)和short(16位,两字节,适用于端口号)

我们可以直接使用而不用担心我们原先的主机字节序是大端还是小端,一般来说,在大端法的主机上,这四个函数是空宏(因为不需要转换)

字节操纵函数

所谓的字节操纵函数,是对某一个地址起头的数据进行设置、复制和比较工作,我们可以用两组函数

第一组函数来自BSD所以是以b开头的,分别是

 	int	 	 bcmp(const void *ptr1, const void *ptr2, size_t nbytes); //比较,如果相同返回0,否则返回非0
    void	 bcopy(const void *src, void *dest, size_t nbytes); // 将src复制到dest中,注意src在前
    void	 bzero(void *dest, size_t nbytes);// 将dest指定nbytes字节给置空为0,一般用于初始化

第二组是ANSI C函数

  	int	 	memcmp(const void *__s1, const void *__s2, size_t __n);
    void	*memcpy(void *__dst, const void *__src, size_t __n);//注意跟bsd的是相反的,dest在前面
    void	*memset(void *__b, int __c, size_t __len);//第二个参数表示统一设置成什么

inet_aton,inet_addr与inet.ntoa函数

显然我们会希望看到类似于123.2.3.2形式的ip地址,但是计算机可能更青睐于网络字节序的32位字符,我们因此需要工具允许我们在之间做转换

 	int		 	 inet_aton(const char *strptr, struct in_addr *addrptr);//该函数首先会校验strptr的ip地址,然后转换为32位的网络字节序的二进制,成功返回1,否则返回0
    in_addr_t	 inet_addr(const char *strptr); //该函数返回值,但是缺点在于,该函数将255.255.255.255对应的32位作为错误码了(INADDR_NONE),导致不能处理广播地址,已被废弃
    char		*inet_ntoa(struct in_addr);// 这个函数可以将字节序的32位数字转换成的点分十进制数串,首先要注意,传入的是结构而不是指针,然后该函数是依赖于静态内存的,所以不可重入,这意味着如果你多次调用该函数,则最后只会保留最后一次调用的结果(前面被覆盖了)

    //inet_ntoa不可重入的例子
    l1= inet_addr("192.168.0.74");
    l2 = inet_addr("211.100.21.179");
    memcpy(&addr1, &l1, 4);
    memcpy(&addr2, &l2, 4);
    printf("%s : %s\n", inet_ntoa(addr1), inet_ntoa(addr2)); //结果相同

一个帮助记忆的原则:a指的是ascii字符串,n指的是numeric数字,所以aton就是字符串到数字

inet_pton与inet_ntop函数

是伴随IPv6诞生的,但是可以兼容ipv4,p指的是presentation,n指的是number


    int		 	inet_pton(int family, const char *strptr, void *addrptr);//family要么是AF_INET,要么是AF_INET6,其他选项都会返回错误,如果strptr对应的字符串跟family不搭,会返回0表示错误,否则设置addrptr同时返回1
    const char	*inet_ntop(int family, const void *addrptr, char *strptr, socklen_t len);//len指示调用者缓冲区大小避免溢出,一般可以用常数指示,如果len传入太小,就会返回空值,如果调用成功,返回的指针就是修改的strptr,常数包括INET_ADDRSTRLEN(默认16)与INET_ADDRSTRLEN(46)
    
    //使用ntop
    char str[INET_ADDRSTRLEN];
    char *ptr = inet_ntop(AF_INET,&foo.sin_addr, str, sizeof(str)); 

readn, writen和readline函数

read和write函数在套接字上表现会变得奇怪,比方说你试图读取n个字节,但是你可能会得到一个小于n的值,这是因为缓冲区可能只有这么多数据,而write也可能返回一个小于指定值的值,这是因为write运行的时候虽然会阻塞等待直到完全写入,但是这个过程中是可能有中断的,而write遇到中断的时候就会推出返回的是当前已经写入的值,所以我们还是需要使用while循环去确保写入

注意的是,当前版本的readline是每次只读取一个字符,导致非常的慢,一个简单的方案是改用stdin,但是stdin虽然可以较快读取,带来的问题确实,由于stdin的缓冲区不可见,于是我们无法获知缓冲区状态从而进行一定的防御性编程

我们可以自行实现一个较快版本的readline,原理其实就是用自己写的缓冲区,并且提供接口获知缓冲区信息

具体的代码示例

int readn(int fd, void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nread;
    char *ptr;

    ptr = vptr;
    nleft = n;
    while (nleft > 0)
    {
        if ((nread = read(fd, ptr, nleft)) < 0)
        {
            if (errno == EINTR) // 被中断了
                nread = 0;
            else
                return -1; // 其他异常,直接退出
        }
        else if (nread == 0)
            break; //eof了所以退出
        nleft = nleft - nread;
        ptr += nread;
    }
    return n - nleft;
}
int writen(int fd, void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nwrite;
    char *ptr;

    ptr = vptr;
    nleft = n;
    while (nleft > 0)
    {
        if ((nwrite = write(fd, ptr, nleft)) <= 0)
        {
            if (nwrite < 0 && errno == EINTR) // 系统调用被中断了,重新执行
                nwrite = 0;
            else
                return -1; // 其他异常,直接退出
        }
        nleft = nleft - nwrite;
        ptr += nwrite;
    }
    return n - nleft;
}
ssize_t readline(int fd, void *vptr, size_t max_len)
{
    size_t n, rc;
    char c, *ptr;
    ptr = vptr;
    for (n = 1; n < max_len; n++)
    {
    again:
        if ((rc = read(fd, c, 1)) == 1)
        {
            *ptr = c;
            ptr++;
            if (c == '\n')
                break;
        }
        else if (rc == 0)
        {
            *ptr = 0;
            return n - 1;
        }
        else
        {
            if (errno == EINTR)// 系统调用被中断了,重新执行
                goto again;
            return (-1);
        }
    }
    *ptr = 0;
    return (n);
}

以及自行实现buffer的readline代码示例

static int read_cnt;
static char *read_ptr;
static char read_buf[MAXLINE];
static ssize_t my_read(int fd, char *ptr)
{
    if (read_cnt <= 0)
    {
    again:
        if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0)
        {
            if (errno == EINTR)
                goto again;
            return -1;
        }
        else if (read_cnt == 0)
            return 0;
        read_ptr = read_buf;
    }
    read_cnt--;
    *ptr = *read_ptr++;
    return (1);
}
ssize_t readline(int fd, void *vptr, size_t max_len)
{
    size_t n, rc;
    char c, *ptr;
    ptr = vptr;
    for (n = 1; n < max_len; n++)
    {
    again:
        if ((rc = my_read(fd, &c)) == 1)
        {
            *ptr = c;
            ptr++;
            if (c == '\n')
                break;
        }
        else if (rc == 0)
        {
            *ptr = 0;
            return n - 1;
        }
        else
        {
            if (errno == EINTR) // 系统调用被中断了,重新执行
                goto again;
            return (-1);
        }
    }
    *ptr = 0;
    return (n);
}
ssize_t readlinebuf(void **vptrptr)
{
    if (read_cnt) //>0
        *vptrptr = read_ptr;
    return read_cnt;
}
发布了31 篇原创文章 · 获赞 32 · 访问量 747

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/103175430
今日推荐