首页 > 代码库 > socket通信

socket通信

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_INETAF_INET6等格式。
  type:指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等。
  protocol:顾名思义,就是指定协议。套接口所用的协议。如调用者不想指定,可用0。常用的协议有,IPPROTO_TCPIPPROTO_UDPIPPROTO_STCPIPPROTO_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: 出错。


[1]:这里的长度不能多也不能少,笔者在这里预留多余长度后在后台解析错误,特此批注。 ?

 

socket通信