嵌入式 Linux C ARM Linux学习之路Linux学习|Gentoo/Arch/FreeBSD

网络编程

2020-03-26  本文已影响0人  Leon_Geo

1.全球IP因特网

1.1数据在互联网上的传输过程

image

1.2 一个网络程序的软硬件组织

image

1.3 IP地址结构

struct in_addr{
    uint32_t s_addr;    //大端法表示的IP地址
};
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);     //返回网络字节序

uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);      //返回主机字节序

1.4因特网域名

为了便于人类的记忆,DNS服务器记录了所有IP地址和主机名的映射,我们可以通过Linux系统下的nslookup程序来查看域名对应的地址。

linux>    nslookup localhost
Address:127.0.0.1    

命令行下输入:hostname会得到本机的IP地址。

1.5 地址转换的示例

#include <stdio.h>
#include <sys/inet.h>
#include "csapp.h"

int main(int argc, char **argv)
{
    struct in_addr inaddr;
    uint32_t addr;
    char buf[MAXBUF];
  
    if(argc != 2){
        fprintf(stderr, "usage:%s <hex num>\n",argv[0]);
        exit(0);
    }
    sscanf(argv[1],"%x",&addr);
    inaddr.s_addr = htonl(addr);
    
    if(!inet_ntop(AF_INET,&inaddr,buf,MAXBUF))
        unix_error("inet_ntop");
    printf("%s\n",buf);
    exit(0);
}

2.套接字接口

所谓套接字接口其实是一组函数,它们和I/O函数结合起来,用以创建网络应用。下图是基于套接口的网络应用概述:

image

2.1 套接字地址结构

从程序的角度看,套接字就是一个打开文件的描述符。套接字地址存放在类型为sockaddr_in的16字节结构体中。

/*IP套接字地址结构,_in是互联网的缩写*/
struct sockaddr_in{
    uint16_t        sin_family; //协议簇(AF_INET或AF_INET6)
    uint16_t        sin_port;   //端口号
    struct in_addr  sin_addr;   //IP地址
    unsigned char   sin_zero[8];//为了与struct sockaddr边界对齐而填充的字节
};
  
/*通用套接字地址结构*/
struct sockaddr{
    uint16_t        sa_family;  //协议簇(AF_INET或AF_INET6)
    char            sa_data[14];//地址数据
};

为什么会出现两种套接字地址?

网络编程函数connect、bind和accept要求一个指向与协议有关的套接字地址结构的指针。但套接字接口设计者面临的问题是如何定义这些函数,使之能够接受各种类型的套接字地址结构。而当时void *指针还没有发明,所以解决办法是设计的套接字函数都采用通用地址结构作为参数,而所有特定协议的套接字指针在使用时都强制转换成通用结构。

为了简化代码,Steven指导定义:

typedef struct sockaddr SA;

然后,无论何时需要将sockaddr_in结构强制转换成sockaddr结构时,我们都使用(SA)。

2.2 socket

该函数创建一个套接字描述符;

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol); //成功返回非负描述符,错误返回-1

最好的方法是利用getaddrinfo函数自动生成这些参数,以后会说。这里函数返回的套接字描述符仅是部分打开的,还不能用于读和写。

2.3 connect

客户端利用该函数建立与服务器的连接。

#include <sys/socket.h>

int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
                                                    //成功返回0,错误返回-1

该函数阻塞等待与服务器的连接,若成功就表示套接字描述符现在可以读写了,并且得到的连接是由(客户端IP地址:客户端分配的临时端口号)唯一表示。最好的方法也是利用getaddrinfo函数自动生成这些参数。

2.4 bind

服务器用它将套接字地址和套接字描述符绑定起来。

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
                                                    //成功返回0,错误返回-1

最好的方法也是利用getaddrinfo函数自动生成这些参数。

2.5 listen

服务器调用该函数告诉内核,该描述符是被服务器用来监听来自客服端连接请求的。

#include <sys/socket.h>

int listen(int sockfd, int backlog);
                                                    //成功返回0,错误返回-1

2.6 accept

服务器调用该函数等待来自客服端的连接请求。

#include <sys/socket.h>

int accept(int listenfd, struct sockaddr *addr, int *addrlen);
                                                    //成功返回非负描述符,错误返回-1

该函数等待来自客户端的连接请求到达监听描述符listenfd,然后在addr中填写客服端的套接字地址,并返回一个已连接描述符,而它可以用来与客户端通信。

监听描述符和已连接描述符的区别:

  • 监听描述符作为客户端连接请求的一个端点,通常被创建一次,存在于服务器的整个生命周期;
  • 已连接描述符是客户端与服务器之间已经建立起来的连接的一个端点,服务器每次接受连接请求时都会创建一次,它只存在与服务器为一个客户端服务的过程中。

3.转换函数

3.1 getaddrinfo

函数将主机名(网址或点分十进制IP地址)和服务名(端口号)的字符串转化成套接字地址结构。它是可重入和协议无关的,是代替gethostbyname和getservbyname函数的替代品。

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

struct addrinfo{
    int         ai_flags;   /*hints的参数*/
    int         ai_family;  /*套接字函数的第一个参数*/
    int         ai_socktype;/*套接字函数的第二个参数*/
    int         ai_protocol;/*套接字函数的第三个参数*/
    char        *ai_canonname;/*主机的官方名*/
    size_t      ai_addrlen; /*套接字地址长度*/
    struct sockaddr *ai_addr; /*套接字地址指针*/
    struct addrinfo *ai_next; /*指向下一个addrinfo条目*/
};


int getaddrinfo( const char *host, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **result);      //成功返回0,错误返回非零错误代码。

void freeaddrinfo(struct addrinfo *result);     //无返回

const char *gai_strerror(int errcode);          //返回错误消息字符串

getaddrinfo函数返回的addrinfo结构中的ai_addr指向的套接字地址可以直接用来传递给套接字接口中的函数(socket、connect、bind、listen、accept等),该特点使得我们编写的客户端和服务器能够独立于某个特殊版本的IP协议。下图展示了getaddrinfo返回的数据结构:

image
  • 为了避免内存泄漏,一般在调用完getaddrinfo函数之后,会调用freeaddrinfo函数释放该链表;
  • 如果getaddrinfo遇到错误,应用程序可以调用gai_strerror函数将错误代码转换成字符串。
  • 当getaddrinfo创建列表中的addrinfo结构时,会填写除了ai_flags的每个字段。

3.2 getnameinfo

函数将一个套接字地址结构转化成相应的主机名(网址或点分十进制IP地址)和服务名(端口号)字符串,并将它们复制到host和service缓冲区。它也是可重入和协议无关的,是代替gethostbyaddr和getservbyport函数的替代品。

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getnameinfo( const struct sockaddr *sa, socklen_t salen,
                char *host, size_t hostlen,
                char *service, size_t servlen, int flags);
                //成功返回0,错误返回非零错误代码。

3.3 域名翻译程序

利用上两节所学的两个函数,编写程序hostinfo.c,当输入一个域名时,会得到相应的点分十进制IPv4地址。

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int main(int argc, char **argv) 
{
    struct addrinfo *p, *listp, hints;
    char buf[MAXLINE];
    int rc, flags;

    if (argc != 2) {
    fprintf(stderr, "usage: %s <domain name>\n", argv[0]);
    exit(0);
    }

    /* Get a list of addrinfo records */
    memset(&hints, 0, sizeof(struct addrinfo));                         
    hints.ai_family = AF_INET;       /* IPv4 only */        //line:netp:hostinfo:family
    hints.ai_socktype = SOCK_STREAM; /* Connections only */ //line:netp:hostinfo:socktype
    if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
        exit(1);
    }

    /* Walk the list and display each IP address */
    flags = NI_NUMERICHOST; /* Display address string instead of domain name */
    for (p = listp; p; p = p->ai_next) {
        Getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
        printf("%s\n", buf);
    } 

    /* Clean up */
    Freeaddrinfo(listp);

    exit(0);
}

首先,初始化hints结构,使getaddrinfo返回我们想要的地址。我们想得到用作“连接”的IPv4地址,且只想得到域名,不要服务名。

然后,遍历addrinfo结构链表,用getnameinfo将每个套接字地址转换成IPv4地址字符串。

最后,用freeaddrinfo函数释放链表。

运行程序,我们会看到twitter.com映射到4个IP地址。

linux> ./hostinfo twitter.com
199.16.156.102
199.16.156.230
199.16.156.6
199.16.156.70

4. 套接字接口的辅助函数

套接字接口函数和转换函数看上去有些可怕,相当复杂凌乱。这一节会介绍一些包装函数,它们将会大大简化客户端和服务器通信程序的编写。

4.1 open_clientfd

客户端可以直接利用该函数建立与服务器的连接。

#include "csapp.h"

int open_clientfd(char * hostname, char *port);
                //成功返回套接字描述符,出错返回-1

下面是它的源代码,它是可重入和协议无关的。

int open_clientfd(char *hostname, char *port) {
    int clientfd, rc;
    struct addrinfo hints, *listp, *p;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;  /* Open a connection */
    hints.ai_flags = AI_NUMERICSERV;  /* ... using a numeric port arg. */
    hints.ai_flags |= AI_ADDRCONFIG;  /* Recommended for connections */
    if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo failed (%s:%s): %s\n", hostname, port, gai_strerror(rc));
        return -2;
    }
  
    /* Walk the list for one that we can successfully connect to */
    for (p = listp; p; p = p->ai_next) {
        /* Create a socket descriptor */
        if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) 
            continue; /* Socket failed, try the next */

        /* Connect to the server */
        if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1) 
            break; /* Success */
        if (close(clientfd) < 0) { /* Connect failed, try another */  //line:netp:openclientfd:closefd
            fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
            return -1;
        } 
    } 

    /* Clean up */
    freeaddrinfo(listp);
    if (!p) /* All connects failed */
        return -1;
    else    /* The last connect succeeded */
        return clientfd;
}

假设服务器运行在主机hostname上,并在端口port上监听连接请求。

首先,调用getaddrinfo,返回一个addrinfo结构体链表。遍历该列表依次尝试列表中的每个条目中的ai_addr指向的套接字地址,直到调用socket和connect成功。如果一个失败,则在下一次尝试前关闭掉这个套接字描述符。如果成功建立连接,就释放列表内存,并把套接字描述符返回给客户端,客户端就可以利用它与所有Unix I/O函数与服务器通信了。

4.2 open_listenfd

服务器可以直接利用该函数创建一个监听描述符,准备好建立与客户端的连接。

#include "csapp.h"

int open_listenfd(char *port);
                //成功返回套接字描述符,出错返回-1

open_listenfd函数返回一个打开的监听描述符,且已经准备好在端口port上接受客户端的连接请求。下面是它的源代码:

int open_listenfd(char *port) 
{
    struct addrinfo hints, *listp, *p;
    int listenfd, rc, optval=1;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;             /* Accept connections */
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
    hints.ai_flags |= AI_NUMERICSERV;            /* ... using port number */
    if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
        return -2;
    }

    /* Walk the list for one that we can bind to */
    for (p = listp; p; p = p->ai_next) {
        /* Create a socket descriptor */
        if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) 
            continue;  /* Socket failed, try the next */

        /* Eliminates "Address already in use" error from bind */
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,    //line:netp:csapp:setsockopt
                   (const void *)&optval , sizeof(int));

        /* Bind the descriptor to the address */
        if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
            break; /* Success */
        if (close(listenfd) < 0) { /* Bind failed, try the next */
            fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
            return -1;
        }
    }


    /* Clean up */
    freeaddrinfo(listp);
    if (!p) /* No address worked */
        return -1;

    /* Make it a listening socket ready to accept connection requests */
    if (listen(listenfd, LISTENQ) < 0) {
        close(listenfd);
    return -1;
    }
    return listenfd;
}

首先,调用getaddrinfo,返回一个addrinfo结构体链表。遍历该列表依次尝试列表中的每个条目中的ai_addr指向的套接字地址,直到调用socket和bind成功。如果失败,则在下一次尝试前关闭掉这个套接字描述符。如果成功绑定,就释放列表内存,并调用listen函数将该套接字描述符转换为监听描述符返回给调用者,服务器就可以利用它与所有Unix I/O函数响应客户端了。

  • 我们使用了setsockopt函数来配置服务器,使得服务器能够被终止、重启和立即接受连接。一个重启的服务器默认将在30秒内拒绝客户端的连接请求。关于setsockopt的使用很复杂,将会专门写篇文章来讲解他的使用方法。
  • 我们使用了AI_PASSIVE标志并将host参数设置为NULL,这样每个套接字地址字段都会被设置为通配符地址,表示服务器接受发送到本机所有IP地址的请求。

4.3 编写客户端echo和服务器程序

4.3.1客户端程序echoclient

客户端首先与服务器建立连接,之后进入循环等待从标准输入读取文本行发送给服务器。再等待从服务器取回回送的行,并输出结果到标准输出。

#include "csapp.h"

int main(int argc, char **argv) 
{
    int clientfd;
    char *host, *port, buf[MAXLINE];
    rio_t rio;

    if (argc != 3) {
    fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
    exit(0);
    }
    host = argv[1];
    port = argv[2];

    clientfd = Open_clientfd(host, port);
    Rio_readinitb(&rio, clientfd);

    while (Fgets(buf, MAXLINE, stdin) != NULL) {
    Rio_writen(clientfd, buf, strlen(buf));
    Rio_readlineb(&rio, buf, MAXLINE);
    Fputs(buf, stdout);
    }
    Close(clientfd); //line:netp:echoclient:close
    exit(0);
}

4.3.2 服务器程序echoserver

服务器首先打开监听描述符,进入循环等待与客户端建立连接,连接之后首先输出客户端的域名和IP,之后调用echo函数为其服务。echo函数返回后关闭已连接的描述符,连接终止。

#include "csapp.h"

void echo(int connfd);

int main(int argc, char **argv) 
{
    int listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;  /* Enough space for any address */  //line:netp:echoserveri:sockaddrstorage
    char client_hostname[MAXLINE], client_port[MAXLINE];

    if (argc != 2) {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(0);
    }

    listenfd = Open_listenfd(argv[1]);
    while (1) {
    clientlen = sizeof(struct sockaddr_storage); 
    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
    Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE, 
                 client_port, MAXLINE, 0);
    printf("Connected to (%s, %s)\n", client_hostname, client_port);
    echo(connfd);
    Close(connfd);
    }
    exit(0);
}


void echo(int connfd) 
{
    size_t n; 
    char buf[MAXLINE]; 
    rio_t rio;

    Rio_readinitb(&rio, connfd);
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) { //line:netp:echo:eof
    printf("server received %d bytes\n", (int)n);
    Rio_writen(connfd, buf, n);
    }
}

获取更多知识,请点击关注:
嵌入式Linux&ARM
CSDN博客
简书博客
知乎专栏


上一篇下一篇

猜你喜欢

热点阅读