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 的移植版本。
套接字是在文件 I/O 机制的基础上实现的,包括匿名
和有名
两种文件形式。典型的有名套接字是 /dev/log,它使用的是 Unix 域套接字,守护进程 syslogd(8)
使用它和使用系统日志服务的客户进程通信。下面的内容除非特别注明,否则“套接字”特指匿名套接字。
用于分析 TCP/IP 协议的经典 Unix 工具包括 netcat(1)
和 tcpdump(1)
。前者被称为网络瑞士军刀,可以建立任意基于 TCP/IP 的网络连接并进行输入输出;后者可以把所在网络上的数据流转储到当前的标准输出,这些输出可通过管道线连接到一些文本过滤器之类的程序进行分析。
1. 套接字地址结构
进程标识由两部分组成:
- 一部分是计算机的网络地址,它可以帮助标识网络上我们想与之通信的计算机;
- 另一部分是该计算机上用端口号表示的服务,它可以帮助标识特定的进程。
套接字地址结构可以在两个方向上传递:从进程到内核和从内核到进程。
套接字类型
- 1)流式套接字(SOCK_STREAM):提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收, 对应TCP协议。
- 2)数据报式套接字(SOCK_DGRAM):提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱, 对应UDP协议。
- 3)原始套接字(SOCK_RW):使我们可以跨越传输层直接对IP层进行封装传输.
1.1 字节排序函数
网络上通信的双方可能是异构主机,这意味着可能存在字节顺序(Endianness)的不同。
例如 Motorola 68K 系列、早期的 SPARC 等采用的是大端(big-endian)(或称高地址优先)字节序,即在一个机器字的存储单元上,低字节存在高地址,高字节存在低地址上
;而 Interl X86 等则采用小端(little-endian)(或称低地址优先)字节序,即在一个机器字的存储单元上,低字节存在低地址,高字节存在高地址
;而 ARM, SPARC V9, MIPS 等体系结构可以选择使用大端还是小端模式。它们之间直接通信会得到错误的数据。
术语 “小端” 和 “大端” 表示多个字节值的哪一端(小端或大端)存储在该值的起始地址。
主机字节序(host byte order):某个给定系统中所使用的字节序(可能使用大端或者小端)称为主机字节序。
网络字节序(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);
}
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;
- 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 */
}
数据链路套接字地址结构
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);
这就要求对这些函数的任何调用都必须要将指向特定于协议的套接字地址结构的指针进行类型强制转换(casting),变成指向某个通用套接字地址结构的指针,例如:
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*/
}
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));
- (2) 从内核到进程传递套接字地址结构的函数有 4 个:accept、recvfrom、getsockname和 getpeername。这 4 个函数的其中两个参数是指向某个套接字地址结构的指针和指向表示该结构大小的整数变量的指针。
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);
//返回:指向一个点分十进制数串的指针
- 函数 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
-
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 */
-
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));
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);
}
3. 套接字中read、write和文本交互说明
字节流套接字(例如 TCP 套接字)上的 read 和 write 函数所表现的行为不同于通常的文件 I/O。字节流套接字上调用 read 或 write 输入或输出的字节数可能比请求的数量少,然而这不是出错的状态。这个现象的原因在于内核中用于套接字的缓冲区可能已达到了极限。此时所需的是调用者再次调用 read 或 write 函数,以输入或输出剩余的字节
。
对于文本行交互的应用来说,程序应该按照操作缓冲区而非按照操作文本行来编写。