Socket 02-:套接字编程简介

Socket 02-:套接字编程简介


套接字的原始版本是 BSD 套接字(Berkeley sockets),它是通讯端点的抽象。可用于同一机器上的进程间通讯,典型应用为 Unix 域套接字(Unix domain socket);也可用于通讯网络上任何体系结构的计算机之间的通讯,典型应用为 互联网套接字(Network socket)及在此基础上的 TCP/IP 协议栈(Internet protocol suite)实现。

1983 年,4.2 BSD 发布了基于套接字技术的第一个 TCP/IP 协议栈 API 实现,它成为此后其它系统 TCP/IP 实现的基础。POSIX 的 socket(7)标准是在 4.4 BSD 的基础上制定,微软则于 1990 年代初期在成功移植 BSD 套接字的基础上开发了 winsock,此外使用 TCP/IP 技术进行通讯的各类嵌入式系统也有诸多基于 Socket API 的移植版本。git

套接字是在文件 I/O 机制的基础上实现的,包括匿名有名两种文件形式。典型的有名套接字是 /dev/log,它使用的是 Unix 域套接字,守护进程 syslogd(8) 使用它和使用系统日志服务的客户进程通讯。下面的内容除非特别注明,不然“套接字”特指匿名套接字。web

用于分析 TCP/IP 协议的经典 Unix 工具包括 netcat(1)tcpdump(1)。前者被称为网络瑞士军刀,能够创建任意基于 TCP/IP 的网络链接并进行输入输出;后者能够把所在网络上的数据流转储到当前的标准输出,这些输出可经过管道线链接到一些文本过滤器之类的程序进行分析。编程

1. 套接字地址结构

进程标识由两部分组成:安全

  • 一部分是计算机的网络地址,它能够帮助标识网络上咱们想与之通讯的计算机;
  • 另外一部分是该计算机上用端口号表示的服务,它能够帮助标识特定的进程。

套接字地址结构能够在两个方向上传递:从进程到内核和从内核到进程。网络

套接字类型app

  • 1)流式套接字(SOCK_STREAM):提供面向链接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收, 对应TCP协议。
  • 2)数据报式套接字(SOCK_DGRAM):提供无链接服务。不提供无错保证,数据可能丢失或重复,而且接收顺序混乱, 对应UDP协议。
  • 3)原始套接字(SOCK_RW):使咱们能够跨越传输层直接对IP层进行封装传输.

1.1 字节排序函数

网络上通讯的双方多是异构主机,这意味着可能存在字节顺序(Endianness)的不一样。less

例如 Motorola 68K 系列、早期的 SPARC 等采用的是大端(big-endian)(或称高地址优先)字节序,即在一个机器字的存储单元上,低字节存在高地址,高字节存在低地址上;而 Interl X86 等则采用小端(little-endian)(或称低地址优先)字节序,即在一个机器字的存储单元上,低字节存在低地址,高字节存在高地址;而 ARM, SPARC V9, MIPS 等体系结构能够选择使用大端仍是小端模式。它们之间直接通讯会获得错误的数据。dom

术语 “小端” 和 “大端” 表示多个字节值的哪一端(小端或大端)存储在该值的起始地址。socket

主机字节序(host byte order):某个给定系统中所使用的字节序(可能使用大端或者小端)称为主机字节序。tcp

网络字节序(network byte order):网际协议使用大端字节序来传送这些多字节数。

如下接口函数提供了主机字节序网络字节序的转换。用户没必要关心网络字节顺序是什么,只要数据从主机发送到网络上或者从网络上接收数据时,使用这些函数进行转换,就不用担忧字节顺序错误的问题:

#include <netinet/in.h>
uint32_t htonl(uint32_t hostint32);
                返回值:以网络字节序表示的 32 位整数
uint16_t htons(uint16_t hostint16);
                返回值:以网络字节序表示的 16 位整数
uint32_t ntohl(uint32_t netint32);
                返回值:以主机字节序表示的 32 位整数
uint16_t ntohs(uint16_t netint16);
                返回值:以主机字节序表示的 16 位整数
  • h 表示“主机host”字节序,n 表示“网络network”字节序。
  • l 表示“long”(即 4 字节)整数,s 表示“short”(即 2 字节)整数。这是历史遗留问题,事实上即便在 64位的 Digital Alpha 中,尽管长整数占用 64 位,htonl 和 ntohl 函数操做的仍然是32 位的值。

异种体系结构的不一样字节顺序同时也带来带有位操做的程序的可移植性问题,移植时须要特别注意。

//肯定主机字节序的程序 //**************** //咱们在一个短整数变量中存放 2 字节的值 0x0102,而后查看它的两个连续字节  //c[0](低位) 和 c[1](高位),以此肯定字节序。 //**************** int main(int argc, char **argv) { union { short s; char c[sizeof(short)]; } un; un.s = 0x0102; printf("%s: ", CPU_VENDOR_OS); if (sizeof(short) == 2) { if (un.c[0] == 1 && un.c[1] == 2) printf("big-endian\n"); else if (un.c[0] == 2 && un.c[1] == 1) printf("little-endian\n"); else printf("unknown\n"); } else printf("sizeof(short) = %d\n", sizeof(short)); exit(0); } //肯定主机字节序的程序 //**************** //咱们在一个短整数变量中存放 2 字节的值 0x0102,而后查看它的两个连续字节  //c[0](低位) 和 c[1](高位),以此肯定字节序。 //**************** int main(int argc, char **argv) { union { short s; char c[sizeof(short)]; } un; un.s = 0x0102; printf("%s: ", CPU_VENDOR_OS); if (sizeof(short) == 2) { if (un.c[0] == 1 && un.c[1] == 2) printf("big-endian\n"); else if (un.c[0] == 2 && un.c[1] == 1) printf("little-endian\n"); else printf("unknown\n"); } else printf("sizeof(short) = %d\n", sizeof(short)); exit(0); }

1.2 地址格式

一个套接字绑定的进程,在网络上主要以该进程的主机(或 IP 地址,网络层的标识)、协议(传输层的标识)、端口(应用层的标识)等信息来标识。这些信息在 ipv4 因特网域中,以结构 sockaddr_in 来描述,在 ipv6 中,以结构sockaddr_in6表示,两个结构均被封装到套接字的 sockaddr 结构

IPv4因特网域(AF_INET)中,套接字地址结构 sockaddr_in

#include <netinet/in.h> struct sockaddr_in{ uint8_t sin_len;/* length of structure(16)*/ sa_family_t sin_family; /* address family:AF_INET*/ in_port_t sin_port; /* 16-bit TCP or UDP port number;网络字节序*/ struct in_addr sin_addr; /* 32-bit IPv4 address;网络字节序*/ char sin_zero[8] /* unused */ ... }; 其中,typedef unsigned short sa_family_t; typedef uint16_t in_port_t; struct in_addr{ in_addr_t s_addr;/*IPv4 address*/ }; 其中,typedef uint32_t in_addr_t; #include <netinet/in.h> struct sockaddr_in{ uint8_t sin_len;/* length of structure(16)*/ sa_family_t sin_family; /* address family:AF_INET*/ in_port_t sin_port; /* 16-bit TCP or UDP port number;网络字节序*/ struct in_addr sin_addr; /* 32-bit IPv4 address;网络字节序*/ char sin_zero[8] /* unused */ ... }; 其中,typedef unsigned short sa_family_t; typedef uint16_t in_port_t; struct in_addr{ in_addr_t s_addr;/*IPv4 address*/ }; 其中,typedef uint32_t in_addr_t;
  • POSIX 规范只须要这个结构中的 3 个字段:sin_family、sin_addr 和 sin_port。
  • IPv4 地址和 TCP 或 UDP 端口号在套接字地址结构中老是以网络字节序来存储。
  • sin_zero 字段不曾使用,不过在填写这种套接字地址结构时,咱们老是把该字段置为 0。按照惯例,咱们老是在填写前把整个结构置为 0,而不是单单把 sin_zero 字段置为 0。

IPv6因特网域(AF_INET6)中,套接字地址结构 sockaddr_in6

#include <netinet/in.h>
struct sockaddr_in6{
    uint8_t      sin6_len;     /*length of this struct (28)*/
    sa_family_t  sin6_family; /* address family:AF_INET6*/
    in_port_t    sin6_port; /* port number;网络字节序*/
    
    uint32_t     sin6_flowinfo;/*traffic class and flow info*/
    struct in6_addr sin6_addr; /* IPv6 address;网络字节序*/
    
    uint32_t     sin6_scope_id;/*set of interfaces for scope*/
}

其中,
struct in6_addr{
    uint8_t s6_addr[16];/*IPv6 address*/
}

Unix域套接字地址结构

#include <sys/un.h> struct sockaddr_un{ sa_family_t sun_family; /* AF_LOCAL */ char sun_path[104] /* null-terminated pathname */ } #include <sys/un.h> struct sockaddr_un{ sa_family_t sun_family; /* AF_LOCAL */ char sun_path[104] /* null-terminated pathname */ }

数据链路套接字地址结构

struct sockaddr_dl{ unit8_t sdl_len; sa_family_t sdl_family; /* AF_LINK*/ uint16_t sdl_index; /* system assigned index,if > 0 */ uint8_t sdl_type; /* IFT_ETHER,etc.from <net/if_types.h> */ uint8_t sdl_nlen; /* name length, starting in sdl_data[0] */ uint8_t sdl_alen; /* link-layer address length */ uint8_t sdl_slen; /* link-layer selector length */ char sdl_data[12];/* minimum work area,can be larger; contains i/f name and link-layer address */ } struct sockaddr_dl{ unit8_t sdl_len; sa_family_t sdl_family; /* AF_LINK*/ uint16_t sdl_index; /* system assigned index,if > 0 */ uint8_t sdl_type; /* IFT_ETHER,etc.from <net/if_types.h> */ uint8_t sdl_nlen; /* name length, starting in sdl_data[0] */ uint8_t sdl_alen; /* link-layer address length */ uint8_t sdl_slen; /* link-layer selector length */ char sdl_data[12];/* minimum work area,can be larger; contains i/f name and link-layer address */ }

通用套接字地址结构 sockaddr

#include <sys/socket.h>
struct sockaddr{
    uint8_t     sa_len;
    sa_family_t sa_family; /* address family*/
    char        sa_data[]; //variable-length address;Linux下为 sa_data[14]
    ...
};

因而套接字函数被定义为以指向某个通用套接字地址结构的一个指针做为其参数之一,这正如bind 函数的 ANSI C 函数原型所示:

int bind(int, struct sockaddr *, socklen_t); int bind(int, struct sockaddr *, socklen_t);

这就要求对这些函数的任何调用都必需要将指向特定于协议的套接字地址结构的指针进行类型强制转换(casting),变成指向某个通用套接字地址结构的指针,例如:

struct sockaddr_in serv; /* IPv4 socket address structure */ /* fill in serv{} */ bind( sockfd, (struct sockaddr *) &serv, sizeof( serv)); struct sockaddr_in serv; /* IPv4 socket address structure */ /* fill in serv{} */ bind( sockfd, (struct sockaddr *) &serv, sizeof( serv));

新的通用套接字地址结构

#include <netinet/in.h> struct sockaddr_storage{ uint8_t sa_len;/* length of this struct(implementation dependent)*/ sa_family_t sa_family;/* address family:AF_xxx value*/ } #include <netinet/in.h> struct sockaddr_storage{ uint8_t sa_len;/* length of this struct(implementation dependent)*/ sa_family_t sa_family;/* address family:AF_xxx value*/ }

sockaddr_ storage 类型提供的通用套接字地址结构相比 sockaddr 存在如下两点差异。

  • (1) 若是系统支持的任何套接字地址结构有对齐须要,那么 sockaddr_storage 可以知足最苛刻的对齐要求。
  • (2) sockaddr_storage 足够大,可以容纳系统支持的任何套接字地址结构。

套接字地址结构的比较

在这里插入图片描述

  • 前两种套接字地址结构是固定长度的,而 Unix 域结构和数据链路结构是可变长度的。

1.3 值-结果参数

套接字地址结构(sockaddr_in)一直在内核和进程地址空间之间进行传递。当从用户进程向 内核空间传递时(例如 bind,connect,sendto),其中一个参数是套接字地址结构的大小,从 而告诉内核从用户进程到内核确切拷贝多少数据。而当从内核空间向用户进程传递时(accept,recvfrom,getsockname,getpeername)时,传递的是表示结构大小的整数的指针。这是一个**“值-结果”参数**。

  • (1) 从进程到内核传递套接字地址结构的函数有 3 个:bind、connect 和 sendto。这些 函数的一个参数是指向某个套接字地址结构的指针,另外一个参数是该结构的整数大小,例如:
struct sockaddr_in serv; connect(sockfd,(sock_addr *)&serv,sizeof(serv)); struct sockaddr_in serv; connect(sockfd,(sock_addr *)&serv,sizeof(serv));
  • (2) 从内核到进程传递套接字地址结构的函数有 4 个:accept、recvfrom、getsockname和 getpeername。这 4 个函数的其中两个参数是指向某个套接字地址结构的指针和指向表示该结构大小的整数变量的指针。
len = sizeof(cli); getpeername(unixfd,(sock_addr *)&cli,&len); len = sizeof(cli); getpeername(unixfd,(sock_addr *)&cli,&len);

2. 地址转换函数。

  • inet_aton 和 inet_ntoa,用于二进制地址格式和点分十进制字符串表示(如“192.168.1.11”),仅用于IPv4.记法:a表示“ASCII”,n表示"numeric"。
  • inet_ntop和inet_pton,支持IPv4和IPv6地址。记法:p表示"presentation"(地址的表示式一般是ASCII),n表示"numeric"。

inet_aton 和 inet_ntoa函数

#include <arpa/inet.h> int inet_aton( const char *strptr, struct in_addr *addrptr); //返回:若字符串有效则为 1,不然为 0  in_addr_t inet_addr( const char *strptr); //该函数已经弃用了 //返回:若字符串有效则为 32 位二进制网络字节序的 IPv4 地址,不然为INADDR_ NONE  char *inet_ntoa( struct in_addr inaddr); //返回:指向一个点分十进制数串的指针 #include <arpa/inet.h> int inet_aton( const char *strptr, struct in_addr *addrptr); //返回:若字符串有效则为 1,不然为 0  in_addr_t inet_addr( const char *strptr); //该函数已经弃用了 //返回:若字符串有效则为 32 位二进制网络字节序的 IPv4 地址,不然为INADDR_ NONE  char *inet_ntoa( struct in_addr inaddr); //返回:指向一个点分十进制数串的指针
  • 函数 inet_aton 将 strptr 所指 C 字符串转换成一个 32 位的网络字节序二进制值,并经过指针 addrptr 来存储。若是 addrptr 指针为空,那么该函数仍然对输入的字符串执行有效性检查,可是不存储任何结果。
  • 函数 inet_ntoa 将一个 32 位的网络字节序二进制 IPv4 地址转换成相应的点分十进制数串。由该函数的返回值所指向的字符串驻留在静态内存中。这意味着该函数是不可重入的。

inet_ntop和inet_pton函数

#include <arpa/inet.h> /* 将文本字符串格式转换为网络字节序的二进制地址 */ int inet_pton(int family, const char* restrict str, void* restrict addr); //返回值:若成功,返回 1;若格式无效,返回 0;若出错,返回 -1 /* 将网络字节序的二进制地址转换为文本字符串格式 */ const char* inet_ntop(int family, const void *restrict addr, char* restrict str, socklen_t size); //返回值:若成功,返回地址字符串指针;若出错,返回 NULL #include <arpa/inet.h> /* 将文本字符串格式转换为网络字节序的二进制地址 */ int inet_pton(int family, const char* restrict str, void* restrict addr); //返回值:若成功,返回 1;若格式无效,返回 0;若出错,返回 -1 /* 将网络字节序的二进制地址转换为文本字符串格式 */ const char* inet_ntop(int family, const void *restrict addr, char* restrict str, socklen_t size); //返回值:若成功,返回地址字符串指针;若出错,返回 NULL
  • family: AF_INET或者AF_INET6。若是以不被支持的地址族做为 family 参数,这两个函数就都返回一个错误,并将 errno 置为 EAFNOSUPPORT。

  • size: 指定保存文本字符串缓冲区 str 的大小,该参数为 INET_ADDRSTREAM 或者 INET6_ADDRSTREAM 时,代表使用足够大的空间来存放该地址。若是 size 过小,不足以容纳表达格式结果( 包括结尾的空字符),那么返回一个空指针,并置 errno 为 ENOSPC。

    #include <netinet/in.h> #define INET_ ADDRSTRLEN     16          /* for IPv4 dotted- decimal */ #define INET6_ ADDRSTRLEN   46          /* for IPv6 hex string */ #include <netinet/in.h> #define INET_ ADDRSTRLEN     16          /* for IPv4 dotted- decimal */ #define INET6_ ADDRSTRLEN   46          /* for IPv6 hex string */
  • inet_pton 的输出为网络字节序,inet_ntop 的输入为网络字节序,要注意转换。

使用方法:

struct sockaddr_in addr; inet_ntop( AF_INET, &addr.sin_addr, str, sizeof( str)); //或为 IPv6 编写以下代码:  struct sockaddr_in6 addr6; inet_ntop( AF_INET6, &addr6.sin6_addr, str, sizeof( str)); struct sockaddr_in addr; inet_ntop( AF_INET, &addr.sin_addr, str, sizeof( str)); //或为 IPv6 编写以下代码:  struct sockaddr_in6 addr6; inet_ntop( AF_INET6, &addr6.sin6_addr, str, sizeof( str));

unp中封装的sock_ntop函数(非库函数)

注意:对结果进行静态存储致使该函数不可重入且非线程安全。

#include "unp.h" #ifdef HAVE_SOCKADDR_DL_STRUCT #include <net/if_dl.h> #endif /* include sock_ntop */ char * sock_ntop(const struct sockaddr *sa, socklen_t salen) { char portstr[8]; static char str[128]; /* Unix domain is largest */ switch (sa->sa_family) { case AF_INET: { struct sockaddr_in *sin = (struct sockaddr_in *) sa; if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL) return(NULL); if (ntohs(sin->sin_port) != 0) { snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port)); strcat(str, portstr); } return(str); } /* end sock_ntop */ #ifdef IPV6 case AF_INET6: { struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa; str[0] = '['; if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL) return(NULL); if (ntohs(sin6->sin6_port) != 0) { snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port)); strcat(str, portstr); return(str); } return (str + 1); } #endif #ifdef AF_UNIX case AF_UNIX: { struct sockaddr_un *unp = (struct sockaddr_un *) sa; /* OK to have no pathname bound to the socket: happens on every connect() unless client calls bind() first. */ if (unp->sun_path[0] == 0) strcpy(str, "(no pathname bound)"); else snprintf(str, sizeof(str), "%s", unp->sun_path); return(str); } #endif #ifdef HAVE_SOCKADDR_DL_STRUCT case AF_LINK: { struct sockaddr_dl *sdl = (struct sockaddr_dl *) sa; if (sdl->sdl_nlen > 0) snprintf(str, sizeof(str), "%*s (index %d)", sdl->sdl_nlen, &sdl->sdl_data[0], sdl->sdl_index); else snprintf(str, sizeof(str), "AF_LINK, index=%d", sdl->sdl_index); return(str); } #endif default: snprintf(str, sizeof(str), "sock_ntop: unknown AF_xxx: %d, len %d", sa->sa_family, salen); return(str); } return (NULL); } char * Sock_ntop(const struct sockaddr *sa, socklen_t salen) { char *ptr; if ( (ptr = sock_ntop(sa, salen)) == NULL) err_sys("sock_ntop error"); /* inet_ntop() sets errno */ return(ptr); } #include "unp.h" #ifdef HAVE_SOCKADDR_DL_STRUCT #include <net/if_dl.h> #endif /* include sock_ntop */ char * sock_ntop(const struct sockaddr *sa, socklen_t salen) { char portstr[8]; static char str[128]; /* Unix domain is largest */ switch (sa->sa_family) { case AF_INET: { struct sockaddr_in *sin = (struct sockaddr_in *) sa; if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL) return(NULL); if (ntohs(sin->sin_port) != 0) { snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port)); strcat(str, portstr); } return(str); } /* end sock_ntop */ #ifdef IPV6 case AF_INET6: { struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa; str[0] = '['; if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL) return(NULL); if (ntohs(sin6->sin6_port) != 0) { snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port)); strcat(str, portstr); return(str); } return (str + 1); } #endif #ifdef AF_UNIX case AF_UNIX: { struct sockaddr_un *unp = (struct sockaddr_un *) sa; /* OK to have no pathname bound to the socket: happens on every connect() unless client calls bind() first. */ if (unp->sun_path[0] == 0) strcpy(str, "(no pathname bound)"); else snprintf(str, sizeof(str), "%s", unp->sun_path); return(str); } #endif #ifdef HAVE_SOCKADDR_DL_STRUCT case AF_LINK: { struct sockaddr_dl *sdl = (struct sockaddr_dl *) sa; if (sdl->sdl_nlen > 0) snprintf(str, sizeof(str), "%*s (index %d)", sdl->sdl_nlen, &sdl->sdl_data[0], sdl->sdl_index); else snprintf(str, sizeof(str), "AF_LINK, index=%d", sdl->sdl_index); return(str); } #endif default: snprintf(str, sizeof(str), "sock_ntop: unknown AF_xxx: %d, len %d", sa->sa_family, salen); return(str); } return (NULL); } char * Sock_ntop(const struct sockaddr *sa, socklen_t salen) { char *ptr; if ( (ptr = sock_ntop(sa, salen)) == NULL) err_sys("sock_ntop error"); /* inet_ntop() sets errno */ return(ptr); }

3. 套接字中read、write和文本交互说明

字节流套接字(例如 TCP 套接字)上的 read 和 write 函数所表现的行为不一样于一般的文件 I/O。字节流套接字上调用 read 或 write 输入或输出的字节数可能比请求的数量少,然而这不是出错的状态。这个现象的缘由在于内核中用于套接字的缓冲区可能已达到了极限。此时所需的是调用者再次调用 read 或 write 函数,以输入或输出剩余的字节

对于文本行交互的应用来讲,程序应该按照操做缓冲区而非按照操做文本行来编写。