iOS - Socket 网络套接字

时间:2024-01-05 20:29:02

1、Socket

  • 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个 Socket。Socket 又称 "套接字",应用程序通常通过 "套接字" 向网络发出请求或者应答网络请求。

  • Socket 的英文原义是 “孔” 或 “插座”。作为 BSD UNIX 的进程通信机制,取后一种意思。通常也称作 "套接字",用于描述 IP 地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在 Internet 上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个 Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket 正如其英文原意那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供 220 伏交流电,有的提供 110 伏交流电,有的则提供有线电视节目。客户软件将插头插到不同编号的插座,就可以得到不同的服务。

  • Socket 就是为网络服务提供的一种机制。
  • 在 Unix中,网络即是 Socket,并不局限在 TCP/UDP。
  • Socket 可以用于自定义协议。
  • 通信的两端都是 Socket。
  • 网络通信其实就是 Socket 间的通信。
  • 数据在两个 Socket 间通过 IO 传输。
  • Socket 开始是纯 C 语言的,是跨平台的。

1.1 网络

  • 1、IP 地址(主机名):

    • 网络中设备的唯一标示。不易记忆,可以用主机名(域名)。

    • 1) IP V4:

      • 0~255.0~255.0~255.0~255 ,共有 2\^8\^4 = 2\^32 = 42 亿。
    • 2) 本地回环地址:

      • 每台机器都有自己的本地回环地址,ip 为 127.0.0.1 ,主机名为 localhost。如果 127.0.0.1 ping 不通,则网卡不正常。

      • 本地 hosts 文件修改,终端:

        • $ cd /etc
        • $ sudo vim hosts
        • $ 输入密码进入 hosts 文件编辑界面
        • $ 将光标移动到指定位置
          • 英文输入模式下按 i 键进入编辑状态,
          • 英文输入模式下按 esc 键进入命令状态,
          • 在命令状态下输入 :wq 回车,保存退出 hosts 文件。
  • 2、端口号:

    • 用于标示进程的逻辑地址,不同进程的标示。
    • 有效端口为 0 ~ 65535,其中 0 ~ 1024 由系统使用或者保留端口,开发中不要使用 1024 以下的端口。

    • 1) Netcat 的使用:

      • Netcat 是 Mac 终端下用于调试和检查网络的工具包,可用于创建 TCP/IP 连接。
      • 终端:$ nc -lk 12345,开启监听,终端将始终监听本地计算机 12345 端口的数据。
  • 3、传输协议(通讯的规则):

    • 1) TCP:传输控制协议:

      • 建立连接,形成传输数据的通道(建立连接的三次握手,断开连接的四次握手)。
      • 在连接中进行大数据传输,数据大小不收限制。
      • 通过三次握手完成连接,是可靠协议,数据安全送达。
      • 必须建立连接,效率会稍低。
    • 2) UDP:用户数据报协议:

      • 只管发送,不确认对方是否接收到。
      • 不需要建立连接,将数据及源和目的封装成数据包中,每个数据报的大小限制在 64K 之内。
      • 因为无需连接,因此是不可靠协议。
      • 不需要建立连接,速度快。
      • 应用场景:多媒体教室/网络流媒体。
    • 3) 常见网络协议:

      应用层协议 端口 说明
      HTTP 80 超文本传输协议
      HTTPS 443 HTTP+SSL,HTTP 的安全版
      FTP 20, 21, 990 文件传输
      POP3 110 邮局协议
      SMTP 25 简单邮件传输协议
      telnet 23 远程终端协议
  • 4、网络参考模型:

    ISO 参考模型 TCP/IP 参考 说明
    应用层 应用层
    表示层
    会话层
    传输层 传输层 Socket 开发,TCP 协议,UDP 协议
    网络层 网络互连层 路由器,IP 协议
    数据链路层 网络接口层 交换机
    物理层 网线

1.2 Socket 通讯示意图


iOS - Socket		网络套接字

1.3 Socket 连接过程

  • 根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。

    • 1) 服务器监听:

      • 是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
    • 2) 客户端请求:

      • 是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
    • 3) 连接确认:

      • 是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

1.4 Socket 常用函数

  • 1) 创建:

        函数原型:
            int socket(int domain, int type, int protocol);
    
        参数说明:
            domain:协议域,又称协议族(family)。常用的协议族有 AF_INET(ipv4)、
                                                             AF_INET6(ipv6)、
                                                             AF_LOCAL(或称 AF_UNIX,Unix 域 Socket)、
                                                             AF_ROUTE 等。
    
                    协议族决定了 socket 的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4 地址
                (32 位的)与端口号(16 位的)的组合、AF_UNIX 决定了要用一个绝对路径名作为地址。
    
            type:指定 Socket 类型。常用的 socket 类型有 SOCK_STREAM(流式/TCP)、
                                                       SOCK_DGRAM(数据报式/UDP)、
                                                       SOCK_RAW、
                                                       SOCK_PACKET、
                                                       SOCK_SEQPACKET 等。
    
                    流式 Socket(SOCK_STREAM)是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用。数据报式
                Socket(SOCK_DGRAM)是一种无连接的 Socket,对应于无连接的 UDP 服务应用。
    
            protocol:指定协议。常用协议有 IPPROTO_TCP(TCP 传输协议)、
                                         IPPROTO_UDP(UDP 传输协议)、
                                         IPPROTO_STCP(STCP 传输协议)、
                                         IPPROTO_TIPC(TIPC 传输协议)等,
    
            注意:1. type 和 protocol 不可以随意组合,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 组合。当第三个参数为 0 时,
                    会自动选择第二个参数类型对应的默认协议。
                  2. Windows Socket 下 protocol 参数中不存在 IPPROTO_STCP。
              
        返回值:
                如果调用成功就返回新创建的套接字的描述符,如果失败就返回 INVALID_SOCKET(Linux 下失败返回 -1)。套接字描
            述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应
            关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找
            到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓
            冲里。
  • 2) 绑定:

        函数原型:
            int bind(SOCKET socket, const struct sockaddr* address, socklen_t address_len);
    
        参数说明:
            socket:是一个套接字描述符。
            address:是一个 sockaddr 结构指针,该结构中包含了要结合的地址和端口号。
            address_len:确定 address 缓冲区的长度。
    
        返回值:
            如果函数执行成功,返回值为 0,否则为 SOCKET_ERROR。
  • 3) 接收:

        函数原型:
            int recv(SOCKET socket, char FAR* buf, int len, int flags);
    
        参数说明:
            socket:一个标识已连接套接口的描述字。
            buf:用于接收数据的缓冲区。
            len:缓冲区长度。
            flags:指定调用方式。取值:MSG_PEEK 查看当前数据,数据将被复制到缓冲区中,但并不从输入队列中删除;
                                   MSG_OOB 指示接收到 out-of-band 数据(即需要优先处理的数据)。
    
        返回值:
                若无错误发生,recv() 返回读入的字节数。如果连接已中止,返回 0。否则的话,返回 SOCKET_ERROR 错误,应用程序
            可通过 WSAGetLastError() 获取相应错误代码。
    
        函数原型:
            ssize_t recvfrom(int sockfd, void buf, int len, unsigned int flags, struct socketaddr* from, socket_t* fromlen);
    
        参数说明:
            sockfd:标识一个已连接套接口的描述字。
            buf:接收数据缓冲区。
            len:缓冲区长度。
            flags:调用操作方式。是以下一个或者多个标志的组合体,可通过 or 操作连在一起:
                  (1)MSG_DONTWAIT:操作不会被阻塞;
                  (2)MSG_ERRQUEUE:指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传
                                    递进来,使用者应该提供足够大的缓冲区。导致错误的原封包通过 msg_iovec 作为一般的数据
                                    来传递。导致错误的数据报原目标地址作为 msg_name 被提供。错误以 sock_extended_err
                                    结构形态被使用。
                  (3)MSG_PEEK:指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。
                  (4)MSG_TRUNC:返回封包的实际长度,即使它比所提供的缓冲区更长, 只对 packet 套接字有效。
                  (5)MSG_WAITALL:要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者
                                   下次被接收的数据类型不同,仍会返回少于请求量的数据。
                  (6)MSG_EOR:指示记录的结束,返回的数据完成一个记录。
                  (7)MSG_CTRUNC:指明由于缓冲区空间不足,一些控制数据已被丢弃。
                  (8)MSG_OOB:指示接收到 out-of-band 数据(即需要优先处理的数据)。
                  (9)MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其它数据。
            from:(可选)指针,指向装有源地址的缓冲区。
            fromlen:(可选)指针,指向from缓冲区长度值。
  • 4) 发送:

        函数原型:
            int sendto(SOCKET s, const char FAR* buf, int size, int flags, const struct sockaddr FAR* to, int tolen);
    
        参数说明:
            s:套接字
            buf:待发送数据的缓冲区
            size:缓冲区长度
            flags:调用方式标志位, 一般为 0, 改变 Flags,将会改变 Sendto 发送的形式
            addr:(可选)指针,指向目的套接字的地址
            tolen:addr 所指地址的长度
    
        返回值:
            如果成功,则返回发送的字节数,失败则返回 SOCKET_ERROR。
  • 5) 接收连接请求:

        函数原型:
            int accept(int fd, struct socketaddr* addr, socklen_t* len);
    
        参数说明:
            fd:套接字描述符。
            addr:返回连接着的地址
            len:接收返回地址的缓冲区长度
    
        返回值:
            成功返回客户端的文件描述符,失败返回 -1。

2、Socket 的基本使用

  • Objective-C

    • 包含头文件

          #import <sys/socket.h>
          #import <netinet/in.h>
          #import <arpa/inet.h>
    • 创建 Socket

          int socket(int, int, int);
      
          参数:
              int domain  :协议域,AF_INET(IPV4 的网络开发)
              int type    :Socket 类型,SOCK_STREAM(TCP),SOCK_DGRAM(UDP 报文)
              int protocol:IPPROTO_TCP 协议,如果输入 0,可以根据第二个参数,自动选择协议
      
          返回值:
              int         :如果 > 0 就表示成功
      
          int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
      
          if (clientSocket > 0) {
              NSLog(@"Socket 创建成功 %d", clientSocket);
          } else {
              NSLog(@"Socket 创建失败");
          }
    • 连接到服务器

          int connect(int, const struct sockaddr *, socklen_t);
      
          参数:
              int                     :客户端 socket
              const struct sockaddr * :指向数据结构 sockaddr 的指针,其中包括目的端口和 IP 地址,服务器的 "结构体" 地址,C 语言没有对象
              socklen_t               :结构体数据长度
      
          返回值:
              int                     :0 成功,其他 错误代号
      
          // 创建服务器地址结构体,internet style
          struct sockaddr_in serverAddress;
      
          // IPV4 - 协议
          serverAddress.sin_family = AF_INET;
      
          // inet_addr 函数可以把 ip 地址转换成一个整数
          serverAddress.sin_addr.s_addr = inet_addr("127.0.0.1");
      
          // 端口 端口数据高位在前低位在后 12345 => 3039 => 3930
          serverAddress.sin_port = htons(12345);
      
          int isConnected = connect(clientSocket, (const struct sockaddr *)&serverAddress, sizeof(serverAddress));
      
          if (isConnected) {
              NSLog(@"连接失败 %d", isConnected);
          } else {
              NSLog(@"连接成功");
          }
    • 发送数据

          ssize_t send(int, const void *, size_t, int);
      
          参数:
              int          :客户端 socket
              const void * :发送内容地址 void * == id,message.UTF8String 将字符串转换成 UTF8 的 ASCII 码,一个汉字需要 3 个字节
              size_t       :发送内容长度,是字节的个数,strlen 计算所有字节的长度
              int          :发送方式标志,一般为 0
      
          返回值:
              ssize_t      :如果成功,则返回发送的字节数,失败则返回 SOCKET_ERROR
      
          NSString *message = @"约吗?";
      
          ssize_t sendLen = send(clientSocket, message.UTF8String, strlen(message.UTF8String), 0);
      
          NSLog(@"发送 %ld 个字节数据", sendLen);
    • 接收数据

          ssize_t recv(int, void *, size_t, int);
      
          参数:
              int     :客户端 socket
              void *  :接收内容的地址
              size_t  :接收内容的长度
              int     :接收数据的标记,0 是阻塞式,一直等待服务器的数据
      
          返回值:
              ssize_t :接收到的数据长度
      
          // 接收字符串的数组,typedef unsigned char uint8_t;
          uint8_t buffer[1024];
      
          ssize_t recvLen = recv(clientSocket, buffer, sizeof(buffer), 0);
      
          NSLog(@"接收到 %ld 个字节数据", recvLen);
      
          // 按照服务器返回的长度,从 buffer 中,读取二进制数据,建立 NSData 对象
          NSData *data = [NSData dataWithBytes:buffer length:recvLen];
          NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
      
          NSLog(@"接收到:%@", str);
    • 关闭连接

          int  close(int) __DARWIN_ALIAS_C(close);
      
          参数:
              int :客户端 socket
      
          返回值:
              int :0 成功关闭连接
      
          // 数据传递完毕之后,关闭连接
          int isCloseed = close(clientSocket);
      
          NSLog(@"%d", isCloseed);

3、Socket 聊天

  • Objective-C

        // 包含头文件
        #import <sys/socket.h>
        #import <netinet/in.h>
        #import <arpa/inet.h>
    
        @property (weak, nonatomic) IBOutlet UITextField *hostText;
        @property (weak, nonatomic) IBOutlet UITextField *portText;
        @property (weak, nonatomic) IBOutlet UITextField *messageText;
        @property (weak, nonatomic) IBOutlet UILabel *recvLabel;
        @property (weak, nonatomic) IBOutlet UIButton *sendBtn;
    
        ///  client Socket
        @property (nonatomic, assign) int clientSocket;
    
        - (IBAction)conn:(UIButton *)sender {
    
            if (sender.isSelected) {
    
                // 断开连接
                [self disConnection];                                                                   
    
                sender.selected = NO;
                [sender setTitle:@"连接" forState:UIControlStateNormal];
                self.sendBtn.enabled = NO;
    
            } else {
    
                // 创建连接
                if ([self connection:self.hostText.text port:self.portText.text.intValue]) {            
    
                    self.recvLabel.text = @"连接成功";
    
                    sender.selected = YES;
                    [sender setTitle:@"断开" forState:UIControlStateSelected];
                    self.sendBtn.enabled = YES;
    
                } else {
    
                    self.recvLabel.text = @"连接失败";
                }
            }
        }
    
        - (IBAction)send:(UIButton *)sender {
    
            // 发送数据
            self.recvLabel.text = [self sendAndRecv:self.messageText.text];
        }
    
        /// 创建连接
        - (BOOL)connection:(NSString *)hostText port:(int)port {
    
            // 创建 Socket
    
                self.clientSocket = socket(AF_INET, SOCK_STREAM, 0);
    
            // 连接到服务器
    
                struct sockaddr_in serverAddress;
                serverAddress.sin_family = AF_INET;
                serverAddress.sin_addr.s_addr = inet_addr(hostText.UTF8String);
                serverAddress.sin_port = htons(port);
    
                int isConnected = connect(self.clientSocket, (const struct sockaddr *)&serverAddress, sizeof(serverAddress));
    
                return (isConnected == 0);
        }
    
        /// 发送和接收字符串
        - (NSString *)sendAndRecv:(NSString *)message {
    
            // 发送数据
    
                send(self.clientSocket, message.UTF8String, strlen(message.UTF8String), 0);
    
            // 接收数据
    
                uint8_t buffer[1024];                                                                   
    
                ssize_t recvLen = recv(self.clientSocket, buffer, sizeof(buffer), 0);
    
                NSData *data = [NSData dataWithBytes:buffer length:recvLen];
                NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    
                return str;
        }
    
        /// 断开连接
        - (void)disConnection {
    
            close(self.clientSocket);
        }

4、Socket 网络访问

  • 请求:

    • 1、请求行

      • GET / HTTP/1.1

      • 方法 GET
      • 路径 /
      • 协议 HTTP 1.1

    • 2、请求头

      • Host: localhost 主机
      • User-Agent: 告诉服务器客户端的类型
      • Accept: 告诉服务器客户端支持的格式
      • Accept-Language: 告诉服务器客户端的语言
      • Accept-Encoding: 告诉服务器客户端支持的压缩格式
  • 响应:

    • 1、状态行

      • HTTP/1.1 200 OK

        • 协议 HTTP 1.1
        • 状态码:
          • 200 成功
          • 404 页面没找到
          • 301 内容没变化,用在缓存
      • 2、响应头(主要在开发下载应用的时候使用的)

        • Date: Tue, 24 Mar 2015 01:52:25 GMT 访问日期
        • Server: Apache/2.4.9 (Unix) 访问服务器的类型
        • Content-Location: index.html.en 访问的文件名
        • Content-Length: 45 访问文件的大小
        • Content-Type: text/html 访问文件的类型
      • 3、数据实体

        • <html><body><h1>It works!</h1></body></html>

        • 访问服务器最需的,相当于 NSURLConnection 异步方法回调中的 data。

  • Objective-C

        - (void)socketHttpRequest {
    
            // 连接到服务器,80 端口 apache 中就是 http 的协议
            if (![self connection:@"127.0.0.1" port:80]) {
                return;
            }
    
            // 创建请求
            NSString *requestStr = @"GET / HTTP/1.1\n"
            "Host: localhost\n"
            "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:36.0) Gecko/20100101 Firefox/36.0\n"
            "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n"
            "Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3\n"
    
            // \n 拼接换行,内容由 firefox 浏览器的网络控制面板的原始请求头信息中复制
            "Accept-Encoding: gzip, deflate\n"                                                          
    
            // 删除最后三行,最后一行加两个 \n\n
            "Connection: keep-alive\n\n";                                                               
    
            // 发送接收请求,发送给 web 服务器的请求需要遵守 http 协议
            NSString *recvStr = [self sendAndRecv:requestStr];                                          
    
            NSLog(@"%@", recvStr);
        }
    
        /// 创建连接
        - (BOOL)connection:(NSString *)hostText port:(int)port {
    
            // 创建 Socket
    
                self.clientSocket = socket(AF_INET, SOCK_STREAM, 0);
    
            // 连接到服务器
    
                struct sockaddr_in serverAddress;
                serverAddress.sin_family = AF_INET;
                serverAddress.sin_addr.s_addr = inet_addr(hostText.UTF8String);
                serverAddress.sin_port = htons(port);                                                   
    
                int isConnected = connect(self.clientSocket, (const struct sockaddr *)&serverAddress, sizeof(serverAddress));
    
                return (isConnected == 0);
        }
    
        /// 发送和接收字符串
        - (NSString *)sendAndRecv:(NSString *)message {
    
            // 发送数据
    
                send(self.clientSocket, message.UTF8String, strlen(message.UTF8String), 0);
    
            // 接收数据
    
                uint8_t buffer[1024];                                                                   
    
                ssize_t recvLen = recv(self.clientSocket, buffer, sizeof(buffer), 0);
    
                NSData *data = [NSData dataWithBytes:buffer length:recvLen];
                NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    
                return str;
        }