网络编程

常用的数据结构及函数

1. 套接字 socket

什么是套接字:

套接字代表通信的两端点,两端点基于通信网络进行通信,一个套接字要保存的数据就有,源IP地址目的IP地址以及源端口号目的端口号。其用于标识客户端请求的服务器和服务。

套接字利用在主机内核中实现的网络协议栈提供的应用编程接口实现进程通信,对网络内核的实现细节并不关心,只需要网络内核通过足够的通信能力。

套接字创建

套接字在linux系统中表现为文件描述符,形式上有int类型定义的整数表示,套接字的创建可以通过如下linux的系统调用函数来完成。

#include<sys/type.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
  • 返回值:文件描述符表示成功,-1表示错误,error记录错误代码。
  • 头文件sys/type.h对于定义某些C的宏是必要的,通常都需要引用此文件,而sys/socket.h对于定义socket函数时是必须的。
  • 参数:
    • 套接字域名(domain),代表套接字地址族,常用有 ipv4,ipv6。
    • 套接字类型(type),数据传输类型,常用的有 SOCK_STREAM(面向连接)和 SOCK_DGRAM(无连接datagram)。
    • 使用的协议(Protocol),表示传输协议,常用的有 IPPROTO_TCP(TCP协议) 和 IPPTOTO_UDP(UDP协议),一般情况下该参数为0,表示由系统在当前设定的domain下,自动选择最合适的协议类型。
  1. Linux下支持的domain参数的主要有几类:

    域名 地址族
    AF_UNIX,AF_LOCAL 用于本地通信
    AF_INET,PF_INET IPv4,internet协议
    AF_INET6 Ipv6,internet协议
    AF_IPX Novell网络协议
    AF_X25 ITU-T X.25 / ISO-8208协议
  1. 套接字可以用于两台机器上的进程通信,也可哟用于同一台机器上的不同进程间通信 ,AF_LOCAL.
  2. 套接字并不一定需要地址,例如socketpair函数生成一对相互连接但没有地址的套接字,因为这对套接字直接相连,没有必要实现地址,即无名套接字。但不能实现异地主机间的通信。

套接字地址

  1. 通用套接字地址

    #include<sys/socket.h>
    struct socketaddr{
    sa_family_t sa_family; /* 地址族 */
    char sa_data[4] ;/* 地址数据 */

    }
    • 通用类型,实际中并不是直接使用。
    • 任何具体地址类型都必须具有sa_family成员,他决定怎样翻译结构中所包含的地址信息。
  2. IPV4套接字地址

    #include<sys/socket.h>
    struct socketaddr_in{
    sa_family_t san_family; /* 地址族 */
    unit16_t sin_port; /* 端口 */
    struct in_addr_sin_addr; /* ip地址 */
    unsigned char sin_zero[8]; /* 占位字节 */

    }

    struct in_addr{
    uint32_t s_addr; /* ip地址*/
    }

sockaddr_in 结构体中成员描述:

  • sin_family 地址族
  • sin_port 端口号,必须网络字节序形式。
  • sin_addr 具体定义在struct in_addr ,必须是网络字节序形式,代表IP地址,实际为一个32位无符号整数。
  • sin_zero[8] 占位字节,使整个结构以16字节的形式对齐,他并不被使用,所以不需要初始化。
  1. 套接字并不一定需要地址,例如socketpair函数生成一对相互连接但没有地址的套接字,因为这对套接字直接相连,没有必要实现地址,即无名套接字。但不能实现异地主机间的通信。

流式套接字和数据包套接字

  1. 流式套接字

    使用这种套接字时,数据在客户端是顺序发送的,并且到达的顺序是一致的。比如你在客户端先发送1,再发送2,那么在服务器端的接收顺序是先接收到1,再接收到2,流式套接字是可靠的,是面向连接的;

  2. 数据报套接字

    这种套接字是无连接的,数据是打包成数据包发送的,到达的顺序不一定与发送的顺序是一致的,并且数据不一定是可达的,并且接收到的数据还可能出错。检错处理要手动在应用层设置。

2. 使用套接字

包括 创建套接字、引用套接字、套接字I/O操作、半关闭套接字和完全关闭套接字。

创建套接字

#include<sys/type.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);

连接请求函数connect

连接函数connect是属于client端的操作函数,其目的是向服务器端发送连接请求,这也是从客户端发起TCP三次握手请求的开始,服务器端的协议族,网络地址以及端口都会填充到connect函数的serv_addr地址当中。当connect返回0时说明已经connect成功,返回值是-1时,表示connect失败。

#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr * serveraddr,socklen_t addrlen);
  • 返回值: 0表示成功,-1表示失败,error记录错误代码。
  • connect函数用于TCP客服端和TCP服务器间建立连接。该调用包含3个参数。
    • sockfd 调用socket函数生成的套接字。
    • serveraddr 服务器地址
    • addrlen 服务器地址长度

bind绑定本地地址函数

  • 通过socket创建套接字后,该套接字处于未和任何协议地址关联状态,此时虽然仍然可以使用,但是仅限本地一对互联套接字的特殊情况。如果位于两个不同主机的套接字需要连接而又无地址,那么他们就无法通信了。因此套接字创建后,需要执行bind函数,将套接字绑定到指定的协议地址。
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr * myaddr,socklen_t addrlen);
  • 返回值:0表示成功,-1表示失败,error记录错误记录。
  • bind函数参数:
    • sockfd,调用socket函数生成的套接字
    • myaddr,分配给套接字sockfd的地址指针
    • addrlen,myaddr的地址长度

通常TCP客户端套接字并不显示地绑定某个IP地址,而是当连接建立时,由内核根据到达服务器的路由来自动选择地址进行绑定。以下列出执行绑定时对于应用使用不同的IP地址和端口值所具有的含义。

IP地址 端口 含义
INADDR_ANY 0 内核选择IP地址和端口
INADDR_ANY 非0 内核选择IP,应用确定端口
本地IP 0 应用确定IP,内核选择临时端口
本地IP 非0 应用选择IP和端口
  • TCP服务器在启动时将绑定到端口号。如果TCP的客户端或者服务器端不进行绑定操作,则当执行connect或者listen后,有内核负责为此套接字选择一个临时端口。通常客服端不显示绑定端口,除非应用自身要求这样做,而服务器端需要明确进行绑定。
  • 应用进程可以指定套接字绑定到特定的IP地址。对于TCP客户端,绑定的IP地址将作为该套接字所发出IP报文源地址。对于TCP服务器,绑定的IP地址将限制套接字只能接受发送到此IP地址的连接请求。
  • bind其主要的功能是将addrlen长度 structsockaddr类型的myaddr地址与sockfd文件描述符绑定到一起。

绑定时候对于应用使用不同的IP地址和端口号及其含义。

3. 监听函数 listen

#include<sys/socket.h>
int listen(int sockfd,int backlog);
  • 返回值:0表示成功,-1失败,error记录错误代码。
  • 该函数只用于TCP服务器启动监听两个参数
    • 用于监听的套接字 sockfd
    • 连接队列的长度 backlog

backlog参数是指完成TCP三次握手后已经成功建立TCP连接的队列的长度,服务器执行accept操作从该队列中取下一个连接进行后续处理,backlog值默认为128。

  • listen函数不是等一个新的connect的到来,listen的操作就是当有较多的client发起connect时,server端不能及时的处理已经建立的连接,这时就会将connect连接放在等待队列中缓存起来。这个等待队列的长度有listen中的backlog参数来设定。listen和accept函数是服务器模式特有的函数,客户端不需要这个函数。

4. 接受请求函数accept

TCP服务器使用accept函数从backlog队列中返回一个成功建立的连接,如果backlog队列为空,则服务器进程将被阻塞,进入休眠状态。

#include<sys/socket.h>
int accept(int sockfd,struct * cliaddr,socklen_t * addrlen);
  • 返回值:文件描述符表示成功,-1表示失败,error记录错误代码。
  • 参数:
    • sockfd 用于监听的套接字
    • cliaddr 用于接收客户端套接字的地址结构指针
    • addrlen 指向接收的套接字地址缓存最大长度的指针。
  • addrlen指针所指向的整形数作为输入参数,指定了cliaddr的最大长度;作为输出参数,代表函数返回时地址的实际长度
  • 该函数调用成功,返回值是一个新的套接字描述符,称为连接套接字,服务器使用该套接字和已经建立连接的客户端进行通信,而原有的监听套接字继续接受后续新客户端的连接请求。连接套接字在通信完毕后立刻被关闭,但是监听套接字将一直处于监听状态知道整个应用结束。
  • 接受函数accept其实并不是真正的接受,而是客户端向服务器端监听端口发起的连接。对于TCP来说,accept从阻塞状态返回的时候,已经完成了三次握手的操作。Accept其实是取了一个已经处于connected状态的连接,然后把对方的协议族,网络地址以及端口存在了client_addr中,返回一个用于操作的新的文件描述符,该文件描述符表示客户端与服务器端的连接,通过对该文件描述符操作,可以向client端发送和接收数据。同时之前socket创建的sockfd,则继续监听有没有新的连接到达本地端口。返回大于0的文件描述符则表示accept成功,否则失败。
  • listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

backlog队列管理

5. 套接字I/O操作

流式套接字

#include<unistd.h>
ssize_t read(int sockfd,void * buf ,size_t count);

  • 返回值:非0表示所读字节数,0表示文件尾,-1表示失败,error记录错误代码。
ssize_t write(int sockfd,const void *buf ,size_t count);
  • 返回值: 非0表示所写字节数,0表示未写任何数据,-1表示失败,error记录错误的代码。
int close(int sockfd);
int shutdown(int sockfd,int how);
  • 返回值:0表示成功,-1表示失败,error记录错误代码。

read函数有以下参数:

  • sockfd:用于读操作的套接字
  • buf,用于存放读入数据的缓存。
  • count,代表本次read操作可以接收的大量可读数据字节长度,通常为buf所指向的接受缓存的大小。

write参数:

  • sockfd,用于写数据的套接字
  • buf,存放被写数据的缓存
  • count,被写数据字节长度,通常该值为buf指向的输出缓存的大小。

write函数返回值代表实际所写的字节数,该值一般应该等于count,但是有时不相等。

close是完全关闭,而shutdown是希望在关闭本地套接字连接字前仍然可以从远端套接字继续接受数据,但是不允许本地再发送数据。

shutdown参数how表示如何关闭套接字:

说明
0 SHIUT_RD 不允许本地socket进行读操作
1 SHUT_WR 不允许本地socket进行写操作
2 SHUT_RDWR 不允许本地socket进行读和写操作(等于close)

数据报套接字

#include<sys/type.h>
#include<sys/socket.h>
int sendto(int sockfd,const void * buf,int len,int falgs,const struct sockaddr *to,int tolen);
int recvfrom(int sockfd,void * buf,int len,int flags,struct sockaddr * from ,int * fromlen);

  • 返回值:非0表示成功发送或接收的字节数,-1表示失败,error记录错误代码。

  • sendto函数用于向指定的接收者的地址发送数据报,具体参数:

    • sockfd 用于发送数据包的套接字
    • buf 用于存放发送数据的应用缓存指针buf
    • len 数据包消息长度
    • flags 发送选项,普通情况设置为0;
    • to 接收方套接字地址to
    • tolen 接收方地址的长度
  • sendto函数调用成功,返回所发送的数据包字节数(注意并不能保证这些发送成功的数据一定被远端数据包套接字正确接收),当调用发生错误时,返回-1并且error记录错误的原因。

  • recvform函数用于接收数据包数据

    • sockfd 用于接收数据报的套接字
    • buf 用于存放接收数据所在的应用缓存指针buf
    • len 应用缓存的最大长度
    • flags 接收选项,对于普通情况设置为0;
    • from 发送方套接字地址指针from
    • fromlen 接收方地址的长度

6. C/S通信方式

流式套接字通信方式

流式套接字通信方式

数据报套接字通信方式

数据报式套接字通信

7. 字节顺序转换函数

  • 网络字节顺序NBO(Network Byte Order):按从高到低的顺序存储,在网络上使用统一的网络字节顺序,可以避免兼容性问题。

  • 主机字节顺序(HBO,Host Byte Order):不同的机器HBO不相同,与CPU设计有关,数据的顺序是由cpu决定的,而与操作系统无关。

网络字节顺序与本地字节顺序之间的转换函数:

htonl()--"Host to Network Long"
ntohl()--"Network to Host Long"
htons()--"Host to Network Short"
ntohs()--"Network to Host Short"
#include<netinet/in.h>
/* 将一个主机序无符号长整型数据转换为网络字节序无符号长整型数 */
unsigned long htonl(unsigned long host_long);
/* 将一个主机序16位无符号整形数转换为网络字节序16位无符号整数 */
unsigned short htons(unsigned short host_short);
/* 将一个网络字节序无符号长整型数据转换为主机序无符号长整型数据 */
unsigned long ntohl(unsigned long host_long);
/* 将一个网络字节序无符号16位整数转换为主机序无符号16位无符号整形数据 */
unsigned short ntohs(unsigned short host_short);
  • 小端字节序:数据低位存于内存低地址;数据高位存于内存高地址;
  • 大端字节序(网络字节序):数据低位存于内存高地址;数据高位存于内存低地址;(正常人思维)
    大端字节序与小端字节序

8. 地址转换函数

  1. inet_ntop( )

    inet_ntop( ) 和 inet_pton( ) 都是IP地址转换函数,可以在将IP地址在“二进制整数”和“点分十进制”之间转换。而且,这2个函数能够处理 ipv4 和 ipv6 。

    const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);


    int inet_pton(int af, const char *src, void *dst);

    /*这个函数转换字符串到网络地址,第一个参数af是地址簇,第二个参数*src是来源地址,第三个参数* dst接收转换后的数据*/

    inet_ntop这个函数转换网络二进制结构到ASCII类型的地址,参数的作用和inet_pton相同,只是多了一个参数 socklen_t cnt ,他是所指向缓存区 dst 的大小,避免溢出,如果缓存区太小无法存储地址的值,则返回一个空指针,并将 errno 置为 ENOSPC

  2. inet_aton

    #include<netinet/in.h>
    #include<sys/socket.h>
    #include<arpa/inet.h>
    /* 将字符串形式的IP地址转换为无符号32比特整形IP地址 */
    int inet_aton(const char * string ,struct in_addr *in);
    /* 返回值:非0表示IP地址字符串合法,0表示非法 */

    /* 将网络字节序32比特整形数据IP地址 转换为字符串IP形式 */
    char * inet_ntoa(struct in_addr addr)

    struct in_addr {
    in_addr_t s_addr;
    };
    /* in_addr_t 一般为32位的unsigned int,其字节顺序为网络字节序,即该无符号数采用大端字节序。其中每8位表示一个IP地址中的一个数值。*/

    inet_aton参数:

    • string 以点分十进制表示的IP地址字符串。
    • in, 以网络字节序表示的IP地址结构体。(结果)

inet_aton函数的返回值存放在一个静态分配的缓存中,因此随后再次调该函数将导致之前的返回值别覆盖。

  1. inet_addr()
    是将一个点分10进制的IP地址(如192.168.0.1)转换为in_addr中需要的32位无符号IP地址s_addr(0xC0A80001)

举例:


struct in_addr addr1,addr2,addr3;
ulong l1,l2,l3,l4,l5;

l1= inet_addr("4.3.2.255");
int s = inet_aton("4.3.2.255",&addr3);

printf("inet_aton : %d,%x\n",s,addr3.s_addr);
/* 小端模式存储的数据 16进制输出 inet_addr : ff020304 */
for (int i = 0; i < 4; i++) {
int c = addr3.s_addr & 0xFF;
addr3.s_addr= addr3.s_addr >> 8;
printf("%d ",c);
}
/* 小端模式存储的数据 从 数据低位-高位 输出 4 3 2 255 */

printf("\n------\n");
printf("inet_addr : %x\n",l1);
/* 小端模式存储的数据 16进制输出 inet_addr : ff020304 */
l3 =l1;
for (int i = 0; i < 4; i++) {
int c = l3 & 0xFF;
l3 = l3 >> 8;
printf("%d ",c);
}
/* l1 小端模式存储的数据 从 数据低位-高位 输出 4 3 2 255 */
/* l4 大端模式存储的数据 16进制输出 40302ff */

l4 = ntohl(l1);
printf("%x\n",l4);
/* l4 大端模式存储的数据 从 数据低位-高位 输出 255 2 3 4 */
for (int i = 0; i < 4; i++) {
int c = l4 & 0xFF;
l4 = l4 >> 8;
printf("%d ",c);
}
/* l5 大端模式存储的数据 16进制输出 40302ff*/
l5 = htonl(l1);
printf("%x\n",l5);

/*
由结果不难看出,ntohl还是htonl都是将原有的数据字节序颠倒一下。
inet_addr与inet_aton 均是将点分10进制字符串转换为无符号整形数(是主机字节序)。
*/

9. 主机信息函数

  1. gethostbyname
    用域名或主机名获取主机信息

    #include<netdb.h>
    struct hostent * gethostbyname(const char * name);

    返回值:若成功则返回hostent结构体指针,返回NULL标识错误,h_errno记录错误代码

    struct hostent{
    char * h_name; /* 主机的正式名称 */
    char ** h_aliases; /* 主机别名列表*/
    int h_addrtype;/* 主机地址类型 */
    int h_length;/* 地址长度 */
    char **h_addr_list; /* 地址列表,每个IP地址都实际上是以网络字节序表示的32比特整形数 */
    }

    /* 保持向后兼容 */
    #define h_addr h_addr_list[0];

    gethostbyname函数的返回值存放在一个静态分配的缓存中,因此随后再次调用该函数将导致之前的返回值被覆盖。

  2. getservbyname
    通过服务器的名字而不是服务器端口号来获取服务信息,

    #include <netdb.h>
    struct servent *getservbyname(const char *servname, const char *protoname);

    成功返回非空指针,失败返回空指针
    此函数返回一个指向下面所示结构的指针:

    struct servent
    {
    char *s_name; //official service name
    char **s_aliases; // alias list 服务别名
    int s_port;// port number, network-byte order
    char *s_proto;// protocol to use
    };

    服务名必须指定,如果指定了一个协议(即protoname为非空指针),则结果表项也必须有匹配的协议。如果protoname没有指定且服务支持多个协议,则返回哪个端口是依赖于实现的。一般来说这没有关系,因为支持多个协议的服务常常使用相同的TCP和UDP端口号,但并没有保证
    结构servent中我们关心的主要成员是端口号,由于端口号是以网络字节序返回的,在将它存储于套接口地址结构时,绝对不能调用htons

    对此函数的典型调用是:

    struct servent *sptr;
    sptr = getservbyname("domain", "udp"); // DNS using UDP
    sptr = getservbyname("ftp", "tcp");//使用FTP服务且使用 TCP协议传输的服务器
    sptr = getservbyname("ftp", NULL); //FTP using TCP
    sptr = getservbyname("ftp", "udp");// this call will fail

  3. getservbyport
    在给定端口号和可选协议后查找相应的服务信息.

    #include <netdb.h>
    struct servent *getservbyport(int port, const char *protoname);

    成功返回非空指针,失败返回空指针
    port为网络字节序。对此函数的典型调用是:

    struct servent *sptr;
    sptr = getservbyport(htons(53), "udp"); // DNS using UDP
    sptr = getservbyport(htons(21), "tcp");//FTP using TCP
    sptr = getservbyport(htons(21), NULL);//FTP using TCP
    sptr = getservbyport(htons(21), "udp");// this call will fail

  4. getportobyname

    依照通讯协定 (protocol) 的名称来获取该通讯协定的其他资料

    #include <netdb.h>
    struct protoent * getprotobyname( const char *name );
    ```
    + name: 通讯协定名称
    + 传回值: 成功 - 一指向 struct protoent 的指针 失败 - NULL  

    ```c
    /* Description of data base entry for a single service. */
    struct protoent
    {
    char *p_name; /* Official protocol name. */
    char **p_aliases; /* Alias list. */
    int p_proto; /* Protocol number. */
    };

  1. getaddrinfo

    IPv4中使用 gethostbyname()函数完成主机名到地址解析,这个函数仅仅支持IPv4,且不允许调用者指定所需地址类型的任何信息,返回的结构只包含了用于存储IPv4地址的空间。IPv6中引入了getaddrinfo()的新API,它是协议无关的,既可用于IPv4也可用于IPv6。

    int getaddrinfo(const char *restrict host,
    const char *restrict service,
    const struct addrinfo *restrict hint,
    struct addrinfo **restrict res);
    • hostname:一个主机名或者地址串(IPv4的点分十进制串或者IPv6的16进制串)
    • service:服务名可以是十进制的端口号,也可以是已定义的服务名称,如ftp、http等
    • hints:可以是一个空指针,也可以是一个指向某个 addrinfo结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说:如果指定的服务既支持TCP也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。
    • result:本函数通过result指针参数返回一个指向addrinfo结构体链表的指针。
    struct addrinfo {
    int        ai_flags;//指示在getaddrinfo函数中使用的选项的标志。
    int         ai_family;//地址族
    int        ai_socktype;//SOCK_DGRAM、
    int         ai_protocol;//IPPROTO_UDP IPPROTO_TCP
    size_t        ai_addrlen; //指向的缓冲区长度
    char        *ai_canonname; // 主机规范名称
    struct sockaddr   *ai_addr;//地址结构
    struct addrinfo   *ai_next;//指向链表中下一个结构的指针。此参数在链接列表的最后一个addrinfo结构中设置为NULL。
    };

    getaddrinfo使用详情

代码示例

网络编程代码示例 网络编程细节

参考文章: