计算机系统基础-学习记录18-网络程序(2)

网络程序(2)
——网络连接的程序实现

  有了前面提到的概念,就可以开始根据客户-服务器模型,逐步进行网络连接程序的编写了。利用C语言编写的网络程序,大致流程如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iYFpQwIP-1619074877331)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20210421142258054.png)]

  对于客户(左侧的Client)而言,首先需要获取自身的地址信息(getaddrinfo),随后利用这个信息创建一个socket地址(socket),接下来就进入了连接服务器的阶段(connect);而对于服务器(右侧的Server)而言,一开始也是获取自身的地址信息(getaddrinfo)以及创建一个socket地址(socket),随后便将socket地址与socket的descriptor相关联(bind),然后再声明这个descriptor是用于服务器而不是客户的(listen),最后等待客户发来的请求并接受(accept)

  接下来,对这些方法进行一个简单的介绍:

网络连接的基本函数

基本结构

sockaddr

  用于表示socket地址的结构:

struct sockaddr{
    uint16_t sa_family; /*协议*/
    char sa_data[14]; /*地址相关信息*/
};

sockaddr_in

  用于表示IP socket地址的结构:

struct sockaddr_in{
    uint16_t sin_family; /*协议*/
    uint16_t sin_port; /*端口号*/
    struct in_addr sin_addr; /*IP地址*/
    unsigned char sin_zero[8];
};

  sockaddr是sockaddr_in的普遍形式,在void *还没有出现的时候,就是用的sockaddr来避免非sockaddr_in导致的数据类型不一致问题

socket方法

  socket方法会根据输入的domain和类型选项,来打开一个socket并返回对应的descriptor:

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

  当domain指定为AF_INET时,说明使用的是32位IP;而当type指定为SOCK_STREAM时,则说明这个socket将会被指定为连接的终点

connect方法

  指定要连接的服务器的socket地址,就可以在自身的client与服务器之间使用connect建立连接:

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

  其中,addr是关于服务器的socket地址,而addrlen则是sizeof(sockaddr_in)的值,表示数据类型sockaddr_in的大小

  在连接成功或出错前,此方法会一直阻塞

bind方法

  此方法会对server中的socket地址和服务器的socket descriptor进行绑定:

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

  同之前提到的connect方法,addr是关于服务器的socket地址,addrlen则是sizeof(sockaddr_in)的值,表示数据类型sockaddr_in的大小

listen方法

  此方法会声明某个socket descriptor所对应的socket,是服务器而非客户的socket:

int listen(int sockfd, int backlog);

  参数backlog一般会设定为1024,与协议有关

accept方法

  使用accept方法,可以与客户相连接:

int accept(int listenfd, struct sockaddr *addr, int *addrlen);

  其中,listenfd是在listen中传入的sockfd,这个socket已经被指定为了是服务器的socket;而addr则是用于存储客户的socket地址信息,在连接成功后,addr会被填充为客户的socket地址

host与服务(service)的相互转换

  host基本信息可以被转换为socket地址结构,而反过来socket地址结构也可以被转换回host基本信息。这样的转换可以使用getaddrinfo和getnameinfo这两个方法来实现

getaddrinfo方法

  此方法会将host名、host地址、服务名和端口号转换为socket地址的结构:

int getaddrinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **result);

  其中,host和service为socket地址的两个成分,而hint则用于指定一些条件。

  hint所属的addrinfo结构定义如下所示:

struct addrinfo{
    int ai_flags;
    int ai_family; /*socket方法中的第一个参数*/
    int ai_socktype; /*socket方法中的第二个参数*/
    int ai_protocol; /*socket方法中的第三个参数*/
    char *ai_canonname;
    size_t ai_addrlen;
    struct sockaddr *ai_addr;
    struct addrinfo *ai_next;
};

  在使用hint指定条件的时候,只设置ai_family、ai_socktype、ai_protocol和ai_flags这四个参数。其中前三个已经在前面提到过,而最后一个ai_flags则可以进行一些设置,例如设定为AI_NUMERICSERV时表示service参数为一个端口数字编号

  在使用了此方法后,result将会指向一个由多个addrinfo结构连接在一起的链表,遍历这个链表即可得到所有与输入相对应的地址信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-58TCEOB6-1619074877335)(C:\Users\蔡三圈\AppData\Roaming\Typora\typora-user-images\image-20210421153026775.png)]

getnameinfo方法

  getnameinfo方法与getaddrinfo方法相反,是将socket结构转换回来的方法:

int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *service, size_t servlen, int flags);

  sa是待转换的socket地址结构,而host和service则是准备接收到的转换后的信息,与在getaddrinfo中输入的host和service相对应

  下述程序使用了getaddrinfo和getnameinfo方法,实现了从域名到ip地址的转换的功能:

#include<sys/types.h>
#include<sys/socket.h>
#include<netdb.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

#define MAXLINE 8192

int main(int argc, char **argv){
    struct addrinfo hints;
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    struct addrinfo *result;
    if(getaddrinfo(argv[1], NULL, &hints, &result) != 0){
        printf("Error in getaddrinfo!\n");
        exit(0);
    }
    printf("ohhhhh\n");
    int flags = NI_NUMERICHOST;
    struct addrinfo *p;
    p = result;
    char host[MAXLINE];
    while(p != NULL){
        getnameinfo(p->ai_addr, p->ai_addrlen, host, MAXLINE, NULL, 0, flags);
        printf("%s\n", host);
        p = p->ai_next;
    }
    freeaddrinfo(result);
    return 0;
}

  输入:

./a.out www.baidu.com

  得到输出结果:

ohhhhh
14.215.177.38
14.215.177.39

程序实现

  使用前面所提到的那些方法,就可以实现简易的客户host和服务器host了

  接下来将对csapp官方文档中的几个相关程序进行分析,以较为全面地理解网络编程的完整流程

从客户端连接到服务端:open_clientfd

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

  官方提供的open_clientfd方法,输入的参数有两个,分别为服务端的hostname(地址)和port(端口)。而输出的参数则是成功连接到服务端的那个客户端,所对应的descriptor值

  在此方法中,一开始使用了getaddrinfo,试图获取服务端的地址(line 10)。随后对返回的链表中的所有地址,都尝试创建一个socket descriptor(line 18)。创建成功后,便尝试将此descriptor和服务器地址进行connect(line 22)。最后,如果连接成功,则返回descriptor值,作为客户端的socket descriptor

在服务端创建socket并监听:open_listenfd

  在服务端,使用下述代码,等待从客户端传来的消息并进行响应:

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

  open_listenfd方法输入的参数为端口号,这将作为此服务器的端口

  在此方法中,首先也用了getaddrinfo获取了服务端的socket地址(line 11),随后创建socket descriptor(line 19),再将此descriptor绑定在服务器地址上(line 27),绑定成功后,使用listen(line 42)将其设定为服务器对应的socket,最后返回socket descriptor

在客户端向服务器发送消息并接收

#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);
}

  从代码中可以看出,在使用open_clientfd打开客户socket对应的descriptor后,就开始尝试获取用户的输入并向服务器发送消息了。这里获取输入的方式是循环往复的,且在获取输入后会先向socket发送消息,随后再从socket对应的缓存中读取接收到的消息并打印在屏幕(标准输出)上。而如果退出循环,则会在释放descriptor后,再退出程序

在服务端接收消息并作出回复

#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);
    }
}

  服务端在使用open_listenfd创建socket descriptor后,便会进入一个循环。在这个循环中,服务端会不断地等待客户端发来的消息,一旦accept成功,则会将客户端发来的消息打印出来

运行效果

  在一个窗口中运行服务端,将端口设置为20,随后再在另一个窗口中运行客户端,地址设置为127.0.0.1(本地localhost对应的ip),端口也设置为20。运行后即可发现,连接成功:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fhco4dgd-1619074877337)(E:\学习记录\csapp\学习记录18——网络程序(2)\2021-04-22 11-24-41屏幕截图.png)]

  随后在客户端输入消息,即可观察到服务端能够显示出接收到多少字节,同时将消息发回给客户端并在客户端显示传回的消息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ls5hhySr-1619074877341)(E:\学习记录\csapp\学习记录18——网络程序(2)\2021-04-22 11-25-27屏幕截图.png)]

  这样一来,最基本的服务器就算是搭建好了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值