写在前面的话
关于socket的通信基本知识这里不多赘述,有兴趣自行百度。本文重点讲解socket中使用到的结构体及其参数意义。
本文也不讲解具体使用教程,网上一搜一堆,提供笔者参考的一篇实现iOS socket通信。
本人是一名iOS工程师,参考的均为OC语言和C语言中及linux系统中的相关定义,在其他语言或操作平台中或有出入。
socket中用到的头文件
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
⚠️iOS审核要求必须支持ipv6,而ipv6的头文件是<netinet6/in6.h>
,在<netinet/in.h>
的最后有相关定义如下
/* INET6 stuff */
#define __KAME_NETINET_IN_H_INCLUDED_
#include <netinet6/in6.h>
#undef __KAME_NETINET_IN_H_INCLUDED_
其中核心<sys/socket.h>
提供了创建,绑定,连接,监听,断开,发消息等常用函数及基本数据结构,之后将一一介绍。
<netinet/in.h>
提供socket地址数据结构sockaddr_in
的相关定义。
<arpa/inet.h>
提供IP地址转换函数
常用数据结构解析
1、sockaddr
/*
* [XSI] Structure used by kernel to store most addresses.
*/
struct sockaddr {
__uint8_t sa_len; /* total length */
sa_family_t sa_family; /* [XSI] address family */
char sa_data[14]; /* [XSI] addr value (actually larger) */
};
该结构体用于存储地址结构。来自<sys/socket.h>
。
sa_len
表示地址的长度。
sa_family
表示地址族常用的族有ipv4的AF_INET
和ipv6的AF_INET6
。
sa_data[14]
表示地址数据。
2、sockaddr_in
/*
* Socket address, internet style.
*/
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
该结构体是对sockaddr的扩展。来自<netinet/in.h>
。
sin_len
表示长度。
sin_family
表示地址族,sin_port
表示端口号,sin_addr
表示ip地址。
sin_zero[8]
没有实际意义,只是为了跟sockaddr
结构在内存中对齐。
3、in_addr
/*
* Internet address (a structure for historical reasons)
*/
struct in_addr {
in_addr_t s_addr;
};
该结构体用来表示一个32位的IPv4地址。来自<netinet/in.h>
。
常用函数解析
1、初始化函数socket()
int socket( int af, int type, int protocol);
该函数来自<sys/socket.h>
。
af:一个地址描述。支持AF_INET
、AF_INET6
等格式。
type:指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM
)和UDP(SOCK_DGRAM
)。常用的socket类型有,SOCK_STREAM
、SOCK_DGRAM
、SOCK_RAW
、SOCK_PACKET
、SOCK_SEQPACKET
等等。
protocol:顾名思义,就是指定协议。套接口所用的协议。如调用者不想指定,可用0。常用的协议有,IPPROTO_TCP
、IPPROTO_UDP
、IPPROTO_STCP
、IPPROTO_TIPC
等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
当返回结果为-1时表示创建失败。
2、htons()
#define htons(x) __DARWIN_OSSwapInt16(x)
#define __DARWIN_OSSwapInt16(x) _OSSwapInt16(x)
/* Generic byte swapping functions. */
OS_INLINE
uint16_t
_OSSwapInt16(
uint16_t data
)
{
/* Reduces to 'rev16' with clang */
return (uint16_t)(data << 8 | data >> 8);
}
该函数来自<sys/_endian.h>
。
该函数将一个16位数从主机字节顺序转换成网络字节顺序。将高低位互换位置。参数为端口号。
3、inet_addr()
in_addr_t inet_addr(const char *);
该函数来自<arpa/inet.h>
。
inet_addr()的功能是将一个点分十进制的IP转换成一个长整数型数。参数为IP地址。
4、连接函数connect()
int connect(int s, const struct sockaddr * name, int namelen);
该函数来自<sys/socket.h>
。
s:标识一个未连接socket。
name:指向要连接套接字的sockaddr结构体的指针。
namelen:sockaddr结构体的字节长度。
返回值为-1表示连接失败。
5、接收函数recv()
ssize_t recv( int s, void *buf, _size_t len, int flags);
该函数来自<sys/socket.h>
。
s:标识一个已连接socket。
buf:接收到的消息的存储位置。注意大小。
len:接收消息的长度。
当返回值小于0时表示错误,当等于0时表示对端的socket已正常关闭。
6、发送函数send()
ssize_t send(int s, const void * buf, size_t len, int √)
该函数来自<sys/socket.h>
。
s:标识一个已连接socket。
buf:发送的消息的存储位置。注意大小。
len:发送消息的长度。[1]
当返回值为-1时表示错误。
7、绑定函数bind()
int bind( int sockfd , const struct sockaddr * my_addr, socklen_t addrlen)
该函数来自<sys/socket.h>
。
sockfd:标识一未捆绑套接口的描述符。
my_addr:赋予套接口的地址。
addrlen:my_addr结构的长度。
当返回值为-1时表示错误。
8、监听函数listen()
int listen( int sockfd, int backlog)
该函数来自<sys/socket.h>
。
sockfd:用于标识一个已捆绑未连接套接口的描述符。
backlog:等待连接队列的最大长度。
9、接受连接accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
该函数来自<sys/socket.h>
。
sockfd:套接字描述符,该套接口在listen()后监听连接。
addr:(可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。
addrlen:(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数。
10、关闭连接close()
int close(int sockfd)
该函数来自unistd.h
sockfd:套接字描述符。
ipv4与ipv6
由于iOS在审核时必须通过ipv6环境的测试,而通常我们使用的都是ipv4的地址,同时sockaddr_in
只能表示ipv4环境的结构。因此引入了sockaddr_in6
。
struct sockaddr_in6 {
__uint8_t sin6_len; /* length of this struct(sa_family_t) */
sa_family_t sin6_family; /* AF_INET6 (sa_family_t) */
in_port_t sin6_port; /* Transport layer port # (in_port_t) */
__uint32_t sin6_flowinfo; /* IP6 flow information */
struct in6_addr sin6_addr; /* IP6 address */
__uint32_t sin6_scope_id; /* scope zone index */
};
基本结构和sockaddr_in
类似,只是参数名上多了个6
。
这里存在一个地址转换,研究了GCDAsyncSocket的转换方法。这里也建议想省事的同学直接使用该第三方库。
NSMutableArray *addresses = nil;
NSError *error = nil;
NSString *portStr = [NSString stringWithFormat:@"%hu", port];
struct addrinfo hints, *res, *res0;
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0);
if (gai_error) {
error = [self gaiError:gai_error];
} else {
NSUInteger capacity = 0;
for (res = res0; res; res = res->ai_next) {
if (res->ai_family == AF_INET || res->ai_family == AF_INET6) {
capacity++;
}
}
addresses = [NSMutableArray arrayWithCapacity:capacity];
for (res = res0; res; res = res->ai_next) {
if (res->ai_family == AF_INET) {
// Found IPv4 address.
// Wrap the native address structure, and add to results.
NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
[addresses addObject:address4];
} else if (res->ai_family == AF_INET6) {
// Fixes connection issues with IPv6
// https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158
// Found IPv6 address.
// Wrap the native address structure, and add to results.
struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)res->ai_addr;
in_port_t *portPtr = &sockaddr->sin6_port;
if ((portPtr != NULL) && (*portPtr == 0)) {
*portPtr = htons(port);
}
NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen];
[addresses addObject:address6];
}
}
freeaddrinfo(res0);
if ([addresses count] == 0) {
error = [self gaiError:EAI_FAIL];
}
}
这里面主要是addrinfo
结构和getaddrinfo()
函数。
addrinfo
struct addrinfo {
int ai_flags; /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */
int ai_family; /* PF_xxx */
int ai_socktype; /* SOCK_xxx */
int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
socklen_t ai_addrlen; /* length of ai_addr */
char *ai_canonname; /* canonical name for hostname */
struct sockaddr *ai_addr; /* binary address */
struct addrinfo *ai_next; /* next structure in linked list */
};
ai_family:指定了地址族,可取值如下:
名称 | 值 | 意义 |
---|---|---|
AF_INET | 2 | ipv4 |
AF_INET6 | 23 | ipv6 |
AF_UNSPEC | 0 | 协议无关 |
ai_socktype:指定我套接字的类型
名称 | 值 | 意义 |
---|---|---|
SOCK_STREAM | 1 | 流 |
SOCK_DGRAM | 2 | 数据报 |
在AF_INET通信域中套接字类型SOCK_STREAM的默认协议是TCP(传输控制协议)
在AF_INET通信域中套接字类型SOCK_DGRAM的默认协议是UDP(用户数据报协议)
ai_protocol:指定协议类型。可取的值取决于ai_address和ai_socktype的值
ai_flags指定了如何来处理地址和名字。
getaddrinfo()
int getaddrinfo(const char * __restrict, const char * __restrict, const struct addrinfo * __restrict, struct addrinfo ** __restrict);
getaddrinfo函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个sockaddr 结构的链而 不是一个地址清单。它具有协议无关性。
hostname:一个主机名或者地址串(IPv4的点分十进制串或者IPv6的16进制串)
service:一个服务名或者10进制端口号数串。
hints:可以是一个空指针,也可以是一个指向某个addrinfo结构的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说:如果指定的服务既支持TCP也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。
返回0: 成功,返回非0: 出错。
-
这里的长度不能多也不能少,笔者在这里预留多余长度后在后台解析错误,特此批注。 ↩