网络编程

2018-10-03  本文已影响0人  漫游之光

每个网络应用都是基于客户-服务器模型的。采用这个模型,一个应用是由一个服务器进程和一个或者多个客户端进程组成。服务器管理某种资源,并且通过操作这种资源来为它的客户端提供某种资源。

对主机而言,网络只是又一种I/O设备,是数据源和数据接收方。一个插到I/O总线扩展槽的适配器提供了到网络的物理接口。从网络上接收到的数据从适配器经过I/O和内存总线复制到内存,通常是通过DMA传送。相似地,数据也能从内存复制到网络。

从程序员的角度,我们可以把因特网看做一个世界范围的主机集合,满足以下特性:

IP地址和端口号:

一个IP地址就是一个32位无符号整数。网络程序将IP地址存放在如下所示的IP地址结构中:

struct in_addr{
    unit32_t s_addr;
}

这样有一个问题,TCP/IP协议规定,网络数据流采用大端字节序,即低地址高字节。但在主机上,有可能是大端字节序,也有可能是小端字节序。为了兼容这两种情况,需要提供网络字节序和主机字节序的转换函数。有如下的一些:

/* 主机字节序转化为网络字节序 */
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
/* 网络字节序转化为主机字节序 */
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

h表示host,n表示network,l表示32位长整数,s表示16位短整数。此外,还有一个问题,IP地址通常由点分十进制表示,而非32位无符号数,所以,这里需要对IP地址进行转化的函数:

/* 点分十进制转化为网络ip地址 */
int inet_pton(int af, const char *src, void *dst);
/* 网络ip地址转化为点分十进制 */
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

IP地址可以标识互联网上的一台主机,但是,通常在一台主机上都运行着多个进程。为了区分这些进程,需要一个额外的标识,这就是端口号,它在主机上唯一标识一个进程。

socket套接字及其函数

前面说过,网络在主机看来,只是另一种I/O设备,而在Linux系统中,所有的I/O设备都被模型化了文件。所以,在Linux环境下,使用socket标识进程间网络通信的特殊文件类型,本质为内核借助缓冲区形成的伪文件。

socket有其特殊之处:

socket套接字的定义和常用函数如下所示:

struct sockaddr_in{
    unit16_t sin_family;/* 协议名称 */
    uint16_t sin_port;/* 端口号 */
    struct in_addr sin_addr;/* 端口地址 */
    unsigned char sin_zero[8];/* 和struct sockaddr保持一致 */
}

/* 创建一个套接字描述符,类似于文件描述符,同于读写socket */
int socket(int domain, int type, int protocol);
/* 阻塞和套接字地址为addr的服务器建立连接,成功得到套接字描述符和套接字 */
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* 把套接字,套接字描述符绑定在一起 */
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* 将套接字转化为主动套接字 */
int listen(int sockfd, int backlog);
/* 通过一个文件描述符,成功则返回一个文件描述符和对应的socketaddr */
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

值得注意的是,为了兼容不同的套接字地址类型结构,使用了一个struct sockaddr *结构体指针,这个结构体相当于void *,因为写程序的时候,没有void *,所以使用了这样一个结构体来表示通用的类型。

TCP连接

对于网络连接的协议,常用的有两种TCP和UDP。下面先说一下TCP的状态以及连接断开的过程。TCP是面向连接的,所以在收发数据之前,必须先建立连接。在这个过程中,需要有一方先在一个端口上等待连接的建立。

对于关闭来说,服务器和客户端都可以主动发起关闭请求,注意,发出FIN的一端,代表了关闭了写端,但是还能读。而收到FIN回应ACK的一端,是关闭了读端,但还能写。下面假设客户端发起关闭请求:

这里说的是典型的情况,这里主动发起关闭的一端还有两个额外的状态转移,第一个,在FIN_WAIT_1的时候,收到了FIN信号,发送ACK,进入CLOSING信号,表示同时关闭。如果在FIN_WAIT_1的时候,收到FIN和ACK信号,直接可以发送ACK,进入到TIME_WAIT状态。

下面以一个echo的网络程序例子来说明编写socket套接字时应当注意的东西。

#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>

#define SERVER_PORT 9090
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[])
{
    struct sockaddr_in server,client;
    socklen_t clen;
    int sfd,cfd;
    char buf[BUFFER_SIZE];
    int n;/* the number read from client */

    /* initial socket */
    sfd = socket(AF_INET,SOCK_STREAM,0);
    memset(&server,0,sizeof(server));
    server.sin_family = AF_INET;
    /* always remember transfer to internet format */
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    server.sin_port = htons(SERVER_PORT);
    bind(sfd,(struct sockaddr *)&server,sizeof(server));

    /* transfer to active socket */
    listen(sfd,20);
    
    while(1){
       cfd = accept(sfd,(struct sockaddr *)&client,&clen);
        while( (n = read(cfd,buf,BUFFER_SIZE)) > 0 ){
            write(cfd,buf,n);
            write(STDOUT_FILENO,buf,n);
        }
        close(cfd);
    }
   
    close(sfd);
    return 0;
}
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 9090
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[])
{
    struct sockaddr_in server;
    int cfd;
    char buf[BUFFER_SIZE];
    int n;

    cfd = socket(AF_INET,SOCK_STREAM,0);

    memset(&server,0,sizeof(server));
    server.sin_family = AF_INET;
    uint32_t ip;
    inet_pton(AF_INET,SERVER_IP,&ip);
    server.sin_addr.s_addr = ip;
    server.sin_port = htons(SERVER_PORT);
    connect(cfd,(struct sockaddr *)&server,sizeof(server));

    while((n = read(STDIN_FILENO,buf,BUFFER_SIZE)) > 0) {
        write(cfd,buf,n);
        if( (n = read(cfd,buf,BUFFER_SIZE))>0 ){
            write(STDOUT_FILENO,buf,n);
        }
    }    
    close(cfd);
    return 0;
}

在调试程序的时候,可以使用命令:

netstat -apn | grep 端口号

来查看对应端口的状态。这样比较好调试,特别是对于这种没有错误处理的程序。

UDP连接

UDP是不需要维护连接的,所以逻辑比起TCP来说,要简单很多。UDP就好像写信,填上收信人的信息,再加上自己的信息,就可以把信发出去了。而对于收信的人来说,如果要回信,只需要交换收信人和发信人的信息,变更信件的信息,就可以发送出去了。但是UDP协议是不可靠的,保证通讯可靠性的机制需要在应用层实现。下面同样以一个echo程序端来说明。

#include<stdio.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>

#define BUFFERSIZE 1024
#define SERVER_PORT 9090

int main(int argc,const char *argv[]){
    
    struct sockaddr_in server,client;
    int sfd,cfd;
    socklen_t clen; 
    int n;
    char buf[BUFFERSIZE];

    /* initial socket */
    sfd = socket(AF_INET,SOCK_DGRAM,0);
    memset(&server,0,sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    server.sin_port = htons(SERVER_PORT);
    
    /* bind,but no need to call listen */
    bind(sfd,(struct sockaddr *)&server,sizeof(server));
    
    while(1){
        clen = sizeof(client);
        n = recvfrom(sfd,buf,BUFFERSIZE,0,(struct sockaddr *)&client,&clen);
        if( n>0 ){
            write(STDOUT_FILENO,buf,n);
            sendto(sfd,buf,n,0,(struct sockaddr *)&client,sizeof(client));
        }                   
    }

    return 0;
}
#include<stdio.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>

#define SERVER_IP "127.0.0.1"
#define BUFFERSIZE 1024
#define SERVER_PORT 9090

int main(int argc,const char *argv[]){
    
    struct sockaddr_in server,client;
    int sfd,cfd;
    socklen_t clen; 
    int n;
    char buf[BUFFERSIZE];

    /* initial socket */
    sfd = socket(AF_INET,SOCK_DGRAM,0);
    memset(&server,0,sizeof(server));
    server.sin_family = AF_INET;
    inet_pton(AF_INET,SERVER_IP,&server.sin_addr.s_addr);   
    server.sin_port = htons(SERVER_PORT);
    
    while( (n = read(STDIN_FILENO,buf,BUFFERSIZE)) > 0 ){
        sendto(sfd,buf,n,0,(struct sockaddr *)&server,sizeof(server));
        n = recvfrom(sfd,buf,BUFFERSIZE,0,NULL,0);
        if( n > 0 ){
            write(STDOUT_FILENO,buf,n);
        }
    }

    return 0;
}

值得注意的是,在TCP的版本中,如果打开了两个client,第二个client是连接不上的;而在UDP版本中,是可以连接上的。在我个人看来,UDP这个版本,就有点像后面说的多路I/O转接模型的特殊情况,它对所有监听的描述符都采取相同的操作。

主机和服务的转换

Linux提供了一些强大的函数实现二进制套接字地址结构和主机名、主机地址、服务名和端口号的字符串表示之间的相互转化。当和套接字接口一起使用时,这些函数能使我们编写独立于任何特定版本的IP协议的网络程序。

getaddrinfo函数将主机名、主机地址、服务名和端口号的字符串表示转化成套接字地址结构。getaddrinfo的host参数可以是域名,也可以是数字地址。service参数可以是服务名(如http),也可以是十进制端口号。如果不想把主机名转换成地址,可以把host参数设置为NULL。对service来说也是一样。但是必须指定两者其中至少一个。

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

int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
void freeaddrinfo(struct addrinfo *res);
const char *gai_strerror(int errcode);

struct addrinfo {
    int              ai_flags;
    int              ai_family;
    int              ai_socktype;
    int              ai_protocol;
    socklen_t        ai_addrlen;
    struct sockaddr *ai_addr;
    char            *ai_canonname;
    struct addrinfo *ai_next;
};

getnameinfo函数和getaddrinfo函数是相反的,将一个套接字地址结构转换成相应的主机和服务器名字。

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

int getnameinfo(const struct sockaddr *addr, socklen_t addrlen, 
    char *host, socklen_t hostlen, char *serv, socklen_t servlen, int flags);

下面是一个例子,实现了nslookup的功能,即查询一个域名的IP地址。

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

#define MAXLINE 1024

int main(int argc, char const *argv[])
{
    struct addrinfo hints,*p,*listp;
    if(argc != 2){
        fprintf(stderr,"please input domain name\n");
        return 0;
    }
    memset(&hints,0,sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    if( getaddrinfo(argv[1],NULL,&hints,&listp) != 0){
        return 0;
    }
    int flags = NI_NUMERICHOST;
    char buf[MAXLINE];
    for(p = listp;p!=NULL;p=p->ai_next){
        getnameinfo(p->ai_addr,p->ai_addrlen,buf,MAXLINE,NULL,0,flags);
        printf("%s\n",buf);
    }
    freeaddrinfo(listp);
    return 0;
}
上一篇 下一篇

猜你喜欢

热点阅读