目的:
建立一个简单的可以收发简单字符串的TCP socket连接
分析:
本次介绍socket从主要分为四个部分入手,建立socket,发送消息,接受消息以及最后的关闭socket。
整体的TCP socket流程如下图
其中左侧的竖线就是我们客户端需要做的,而小写的单词则是我们会用到的socket函数,大写的单词则是socket的状态。
实现:
零. 导入必要的头文件
#import
#import
#import
一.建立socket
从上图我们可以看到这里面要用的函数有两个
1. int socket(int family, int type, int protocol);
说明:创建socket
family:协议簇或者协议域。
family | 说明 |
---|---|
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | Unix域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 密钥套接字 |
基本我们用的都是AF_INET6,即IPv6。(本次为了方便用的是AF_INET,即IPv4)
- type:套接字类型
type | 说明 |
---|---|
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据包套接字 |
SOCK_EQPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
我们大多数情况下遇到的都是SOCK_STREAM,即字节流套接字
- protocol:协议类型
protocol | 说明 |
---|---|
IPPROTO_TCP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
0 | 选择所给定family和type组合的系统默认值 |
并不是所有的参数都可以任意搭配的,具体如下
AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY | |
---|---|---|---|---|---|
SOCK_STREAM | TCP/SCTP | TCP/SCTP | YES | ||
SOCK_DGRAM | UDP | UDP | YES | ||
SOCK_EQPACKET | SCTP | SCTP | YES | ||
SOCK_RAW | IPv4 | IPv6 | YES | YES |
- 返回值: 若成功则为非负描述符(int),若出错则为-1
根据我们的需求(IPv4,字节流套接字,TCP)得到如下代码
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
或者
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
2. int connect(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen)
说明:socket连接
sockfd:socket描述符
即上面socket函数的返回值servaddr:socket地址结构体指针
1)socket信息本是通过 sockaddr_in 类型的结构体来存储的(IP地址、端口号、协议簇赋)。
2)sockaddr_in结构体需要用bzero进行清零初始化
3)IP地址需要用inet_pton函数格式化(将十进制数串变为正确的格式)
4)最后传入的地址需要强转为 const struct sockaddr*addrlen:socket结构体大小
返回值:若成功则0,若出错则为-1
注意:每次connect出错后必须close当前的socket连接,并重新调用socket
代码如下
// 2.建立连接
// socket地址信息结构体
struct sockaddr_in servaddr;
// 清零操作
bzero(&servaddr, sizeof(servaddr));
// 将IP地址、端口号、协议簇赋值
servaddr.sin_family = family;
servaddr.sin_port = htons(port);
// 将十进制数串变为正确的格式
if (inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);) <= 0) {
NSLog(@"inet_pton error for %s", ipChar);
return;
}
/**
socket连接
@param sockfd socket描述符
@param servaddr socket地址结构体指针
@param addrlen socket结构体大小
@return 若成功则0,若出错则为-1
@warning 每次connect出错后必须close当前的socket连接,并重新调用socket
*/
if (connect(sockfd, (const struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
NSLog(@"connect error");
return;
};
// 将socket标识符赋值到成员变量中去
self.clientId = sockfd;
NSLog(@"连接成功");
我们会发现其中协议簇在多个函数中被用到,且为同一个值。IP地址,端口号不应写死。所以我们封装了如下方法
/**
IPv4连接
*/
- (void)sq_connectSocketIPv4WithIP:(NSString *)ip port:(int)port {
[self sq_connectSocketWithIP:ip port:port family:AF_INET];
}
- (void)sq_connectSocketWithIP:(NSString *)ip port:(int)port family:(int)family {
const char *ipChar = ip.UTF8String;
int sockfd;
if ((sockfd = socket(family, SOCK_STREAM, 0)) < 0) {
NSLog(@"socket error");
return;
}
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = family;
servaddr.sin_port = htons(port);
if (inet_pton(family, ipChar, &servaddr.sin_addr) <= 0) {
NSLog(@"inet_pton error for %s", ipChar);
return;
}
if (connect(sockfd, (const struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
NSLog(@"connect error");
return;
};
self.clientId = sockfd;
NSLog(@"连接成功");
}
注意:IPv6并不是简单地将family传值为AF_INET6,IPv6的套接字信息结构体是sockaddr_in6,还有其他改变。最好的做法是编写“协议无关”的程序。
二.接受消息
ssize_t read(int filedes, void * buff, size_t nbytes)
- 说明:从IO流中读取数据到指定缓冲区
- filedes:socket标识符
- buff:缓冲区
- nbytes: 缓冲区大小
- 返回值:读的字节数,若出错则为-1
- 注意:输出的字节数可能比请求的数量少,原因在于内核中用于套接字的缓冲区可能已经达到了极限。此时需要的是调用者再次调用read函数,以输出剩余的字节。
代码如下:
#define SQ_BUFF_MAX 1024
- (void)sq_receiveMessage2 {
uint8_t buff[SQ_BUFF_MAX];
ssize_t recvLen;
while ((recvLen = read(self.clientId, &buff, SQ_BUFF_MAX)) > 0) {
NSLog(@"接收到:%ld字节", recvLen);
// 数据中转换
NSData *recvData = [NSData dataWithBytes:buff length:recvLen];
NSString *recvStr = [[NSString alloc] initWithData:recvData encoding:NSUTF8StringEncoding];
if (!recvStr) {
NSLog(@"error");
}
NSLog(@"%@",recvStr);
};
if (recvLen < 0) {
NSLog(@"error");
}
}
因为这之中有个死循环所以调用的时候需要异步并发调用
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf sq_receiveMessage];
});
三.发送消息
ssize_t write(int filedes, void * buff, size_t nbytes)
- 说明:将buff数据发送到注入到IO流中
- filedes:socket标识符
- buff:缓冲区
- nbytes: 缓冲区大小
- 返回值:读的字节数,若出错则为-1
- 注意:和read相同,会有返回值小于请求的数据量。
- (void)sq_sendMsg:(NSString *)msg {
const char *msgChar = msg.UTF8String;
ssize_t sendLen = write(self.clientId, msgChar, sizeof(msgChar));
NSLog(@"发送了:%ld字节",sendLen);
}
四.关闭socket
- (void)sq_closeSocket {
if (self.clientId) {
int close_result = close(self.clientId);
if (close_result == -1) {
NSLog(@"socket 关闭失败");
return;
}else{
NSLog(@"socket 关闭成功");
}
}
}
代码地址:
https://github.com/mj230816/SQSocketCode
PS:下次会写GCDAsySocket基本使用,粘包/断包。