socket初探

本章主要探索基于ipv4的socket

socket是基于tcp/ip和udp/ip封装的一套c语言的api,被称为套接字,通信的两端通过其来传递信息

服务端demo —- 客户端demo

注意: 案例demo需要先开启服务端,在开启客户端

Socket

socket的使用分为客户端和服务端,一个客户端只能连接一个服务端,一个服务端能被多个客户端连接

客户端和服务端的初始化和交互步骤,如下图所示

image.png

创建socket

创建socket的时候注意,这是tcp固定写法,且是ipv4协议的,ipv6协议的在固定地方追加6,详细可以参考ipv6的适配,或者参考GCDAsyncSocket(pod导入CocoaAsyncSocket)的源码

 /**
1: 创建socket
参数
 domain:协议域,又称协议族(family)。
 常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。
 协议族决定了socket的地址类型,在通信中必须采用对应的地址,
 如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、
 AF_UNIX决定了要用一个绝对路径名作为地址。
 
 type:指定Socket类型。
 常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。
 流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。
 数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。
 
 protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,
 分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
 注意:1.type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。
 当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。
 返回值:
 如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1)
 */
self.clientId = socket(AF_INET, SOCK_STREAM, 0);
if (self.clientId == -1) {
    NSLog(@"创建socket失败");
    return;
}
复制代码

设置ip信息

设置ip信息为要连接的服务端的ip地址,可以用来保证访问到正确的服务器

/**
 __uint8_t    sin_len;          假如没有这个成员,其所占的一个字节被并入到sin_family成员中
 sa_family_t    sin_family;     一般来说AF_INET(地址族)PF_INET(协议族)
 in_port_t    sin_port;         // 端口
 struct    in_addr sin_addr;    // ip
 char        sin_zero[8];       没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐
 */

struct sockaddr_in socketAddr;
socketAddr.sin_family = AF_INET;
//htons : 将一个无符号短整型的主机数值转换为网络字节顺序,
//不同cpu 是不同的顺序 (big-endian大尾顺序 , little-endian小尾顺序)
socketAddr.sin_port = htons(8040);//设置端口号
//inet_addr是一个计算机函数,功能是将一个点分十进制的IPv4地址转换成一个长整数型数
socketAddr.sin_addr.s_addr =  inet_addr("172.26.105.76"); //设置ip
复制代码

连接socket

开始连接服务器,连接完成后,可以像即使通信一样传递和接收消息了

连接服务器之前,一定要先打开测试服务器,否则会连接失败

/**
 参数
 参数一:套接字描述符
 参数二:指向数据结构sockaddr的指针,其中包括目的端口和IP地址
 参数三:参数二sockaddr的长度,可以通过sizeof(struct sockaddr)获得
 返回值
 成功则返回0,失败返回非0,错误码GetLastError()。
 */
int result = connect(self.clientId, (const struct sockaddr *)&socketAddr, sizeof(socketAddr));

if (result != 0) {
    NSLog(@"连接失败");
    return;
}else {
    NSLog(@"连接成功");
}
复制代码

发送、接收消息

经过了上面的步骤,连接成功后,便可以主动发送和接收消息了,我们连接完成后,可以马上进入接收消息的状态,为了保证及时接收到发送来的消息,需要不停通过recv方法读取缓存区的内容,避免内容堆积

接收消息

下面将接受消息的循环,放到了子线程里面接收了,并开启timer开启runloop(仅仅测试使用,实际可以根据情况设定),也可以通过一个while循环

注意缓存区buffer的大小,代表了一次能读取多少数据,每次读取完的数据会在缓存区被移除,下次读取则是新的内容

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [self reciveMessage];
});
//开始进入接收消息状态
- (void)reciveMessage {
    // 4. 接收数据
    /**
     recv
     参数
     1> 客户端socket
     2> 接收内容缓冲区地址
     3> 接收内容缓存区长度
     4> 接收方式,0表示阻塞,必须等待服务器返回数据
     
     返回值
     如果成功,则返回读入的字节数,失败则返回SOCKET_ERROR
     */
    //可以开启一个子线程,开启runloop在里面定时接收消息
    NSTimer *timer = [NSTimer timerWithTimeInterval:0.2 repeats:YES block:^(NSTimer * _Nonnull timer) {
        uint8_t buffer[1024];
        //实际接收消息的方法,接收的消息在buffer里面,注意buffer的大小,当数据满了数据会丢失
        ssize_t recvLen = recv(self.clientId, buffer, sizeof(buffer), 0);
        if (recvLen == 0) {
            NSLog(@"接收到了空白消息");
            return;
        }
        NSData *recvData = [NSData dataWithBytes:buffer length:recvLen];
        NSString *recvString = [[NSString alloc] initWithData:recvData encoding:NSUTF8StringEncoding];
        
        NSLog(@"接收到消息:%@", recvString);
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
 }
复制代码

发送消息

发送消息相对于读取就更为简单了,直接调用send方法发送接口

const char *msg = self.tfMessage.text.UTF8String;
ssize_t sendLen = send(self.clientId,msg, strlen(msg), 0);
_tfMessage.text = @"";
NSLog(@"发送成功了%ld字节", sendLen);
复制代码

这样便可以实现客户端socket通信了

结束通信

结束通信,只需要拿出要结束的socketId即可,客户端结束,则传入客户端id,服务端结束,则传入服务端socketId,从服务端结束与某一个客户端的连接,则传入那个客户端的socketId即可

close(socketId)
复制代码

补充服务端部分

客户端相对于服务端不一样的就是绑定、监听、接收客户端连接,然后才开始可以接收客户端的信息,或者向某个客户端发送消息

服务端的绑定

与客户端的连接connect类似,只不过调用的是bind传入的是服务端的socketId

int bindResult = bind(self.serverId, (const struct sockaddr *)&socketAddr, sizeof(socketAddr));
if (bindResult == -1) {
    NSLog(@"绑定socket失败");
    return;
}
复制代码

服务端的监听

当服务端绑定socket之后,便可以监听客户端们的连接了,这里可以设定客户端的最大链接数量,以保证服务端的正常运行,避免服务器负载运行

//服务端开启监听,设置最大连接数量设置为5
int listenResult = listen(self.serverId, 5);
if (listenResult == -1) {
    NSLog(@"监听失败");
    return;
}
复制代码

开始等待客户端的连接

服务器开启监听后,可以准备接收客户端的连接,这里直接开启全部数量的连接通道,实际可以根据已连接数量,动态扩张连接渠道,以接收客户端连接,当然最大数量不能改变

//开始接收客户端的连接
for (int i = 0; i < 5; i++) {
    [self acceptClientConnect];
}
    
    //监听客户端的连接
- (void)acceptClientConnect {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        struct sockaddr_in client_address;
        socklen_t address_len;
        // accept函数
        int clientSocketId = accept(self.serverId, 
            (struct sockaddr *)&client_address, &address_len);
        self.currentClientId = clientSocketId;
        
        if (clientSocketId == -1) {
            NSLog(@"接受客户端错误: %u", address_len);
        }else{
            NSLog(@"接受客户端成功");
            [self receiveMessage:clientSocketId];
        }

    });
}
复制代码

最后

就这样,服务端和客户端的socket介绍完毕了

注意:这里探索的是ipv4的socket,如果要兼容ipv6,则可以搜索兼容方案,也可以查看三方源码来解决

演示demo: 服务端demo —- 客户端demo

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享