Socket通信

#Socket(套接字层) Socket就是为网络服务提供的一种机制,通信的两端都是Socket,网络通信其实就是Socket的通信,数据在两个Socket之间通过IO传输。 Socket是纯C语言的,是跨平台的。

Socket的接口函数十分简单,客户端跟服务端都有: socket() 、 connect() 、 write() 、 read() 、close() 方法,根据函数名也能知道函数的功能。 服务端多了三个方法: bind() 、 listen() 、 accept()

先来了解一下大小端定义:

网络字节序与主机字节序: 主机字节序:就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:   a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。   b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。 网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序。

#iOS端Socket的实现

  • 先来定义两个宏
//htons : 将主机的无符号短整形数转换成网络字节顺序 
#define SocketPort htons(8050)
//inet_addr:将字符串形式的IP地址 -> 网络字节顺序  的整型值
#define SocketIP   inet_addr("127.0.0.1")
复制代码

#####客户端

  • Socket的创建
    /**     int    socket(int, int, int);
     * 参数
     *  参数一:协议域,决定了socket的地址类型。AF_INET、AF_INET6、AF_LOCAL、AF_ROUT,在通信中必须采用对应的地址,如AF_INET决定了必须适应IPV4地址和端口号的组合(32位地址+16位端口号),AF_UNIX决定了要用一个绝对的路径名作为地址
     * 参数一:socket类型,常用的有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用
     * 参数一:指定协议,一般传0,系统会根据第二个参数自动赋值
     返回值:
     失败返回-1
     */
    int socketId = socket(AF_INET, SOCK_STREAM, 0);
    if (socketId == -1) {
        NSLog(@"创建socket 失败");
        return;
    }
复制代码
  • Socket的连接
    /**
     __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;
    socketAddr.sin_port     = SocketPort;    
    struct in_addr  socketIn_addr; //127.0.0.1
    socketAddr.sin_addr     = socketIn_addr;

    /**
     参数
     参数一:套接字描述符
     参数二:指向数据结构sockaddr的指针,其中包括目的端口和IP地址
     参数三:参数二sockaddr的长度,可以通过sizeof(struct sockaddr)获得
     返回值
     成功则返回0。
     */
    int result = connect(_clinenId, (const struct sockaddr *)&socketAddr, sizeof(socketAddr));
    if (result != 0) {
        NSLog(@"连接socket 失败");
        return;
    }
复制代码

连接Socket需要在服务端建立一个Socket服务并监听客户端的连接,在MAC下,我们可以通过终端命令 nc -lk 8050 来拦截监听,类似于建立了一个监听 Host:127.0.0.1 端口:8050 的服务器。 打开终端输入命令启动监听:

运行代码可以看到

  • 发送消息
    /**
     参数
     参数一:一个用于标识已连接套接口的描述字。
     参数二:包含待发送数据的缓冲区。
     参数三:缓冲区中数据的长度。
     参数四:调用执行方式。
     
     返回值
     如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR
     一个中文对应 3 个字节!UTF8 编码!
     */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    const char *msg = @"hello server".UTF8String;
    ssize_t sendLen = send(self.socketId, msg, strlen(msg), 0);
    
    NSLog(@"发送了%ld个字节",sendLen);
}

复制代码

运行代码,在终端查看监听到的信息,可以看到已经发送成功并被接收到了:

  • 接收消息 接收消息一般放在子线程
    /**
     参数
     参数一:客户端socket
     参数二:接收内容缓冲区地址
     参数三:接收内容缓存区长度
     参数四:0表示阻塞,必须等待服务器返回数据
     
     返回值
     如果成功,则返回读入的字节数,失败则返回SOCKET_ERROR
     */
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                uint8_t buffer[1024];
                ssize_t receiveLen = read(self.socketId, buffer, sizeof(buffer));
        
                if (receiveLen == 0) {
                    continue;
                }
                
                NSLog(@"接收到了%ld个字节",receiveLen);
                NSData * data = [NSData dataWithBytes:buffer length:receiveLen];
                NSString * str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                NSLog(@"接收到:%@",str);
            }
    });

复制代码

接上一步的基础,我们在终端输入“hello client”,查看打印日志可以发现,消息也被成功的接收了:

  • 关闭Socket 关闭函数比较简单,就直接
    if (self.clinenId) {
        // 7: 关闭socket连接
        int close_result = close(self.clinenId);
        
        if (close_result == -1) {
            NSLog(@"socket 关闭失败");
            return;
        }else{
            NSLog(@"socket 关闭成功");
        }
    }
复制代码

#####服务端 服务端Socket在客户端的基础上增加了 bind() 、 listen() 、 accept()

  • bind() bind()函数把一个地址族中的特定地址赋给socket,例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
 //具体参数跟客户端那边连接Socket时的参数基本一致
    int bind_result = bind(self.serverId, (const struct sockaddr *)&socketAddr, sizeof(socketAddr));
    if (bind_result == -1) {
        NSLog(@"绑定socket 失败");
        return;
    }
复制代码
  • listen() 作为服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
    /*
     * 参数
     * 参数一:socketId
     * 参数二:最大连接数
     **/
    int maxCount = 5;
    int listen_result = listen(self.serverId, maxCount);
    if (listen_result == -1) {
        NSLog(@"监听失败");
        return;
    }
复制代码
  • accept() 依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。 accept一般也放在子线程中进行。
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        struct sockaddr_in clientAddr;
        socklen_t addressLen;
        
        /**
         参数
         参数一:serverSocketId
         参数二:指定用于返回客户端的协议地址
         参数三:客户端协议地址的长度
         return
         如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接 此处用clientId来接收
         */
        self.clientId = accept(self.serverId, (struct sockaddr*)&clientAddr, &addressLen);
        
        if (self.clientId == -1) {
            NSLog(@"客户端error");
        }
        else {
            uint8_t buf[1024] ;
            ssize_t iReturn = read(self.clientId, buf, sizeof(buf));
            
            if (iReturn>0) {
                NSData *recvData  = [NSData dataWithBytes:buf length:iReturn];
                NSString *recvStr = [[NSString alloc] initWithData:recvData encoding:NSUTF8StringEncoding];
                NSLog(@"客户端来消息了 : %@",recvStr);
            }
        }
    });
复制代码

这样我们就实现了简单的Socket客户端和服务端的通讯。感兴趣的可以自己试着手动写个Demo,非常简单实用。

转载于:https://juejin.im/post/5d22f352e51d45108c59a5c4

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值