担当发现传输的难题,只是给程序员提供一个接口

TCP/IP:Transmission Control Protocol/Internet
Protocol,传输控制协议/因特网互联协议,又名互联网通信协议。简单的讲:TCP控制传输数据,负责发现传输的题材,一旦反常就发出信号,要求重新传输,直到全数数据安全科学地传输到指标地,而IP是承担给因特网中的每一台电脑定义三个地址,以便传输。TCP协议在不可胜言分布式应用程序中举行音信命令传递是必不可少的部分。

壹 、概念精晓

1.什么是Socket?

Socket又称为“套接字”,是系统提供的用于互联网通讯的方法,本质并不是1个磋商,没有规定计算机如何传递消息,只是给程序员提供1个接口,使用那一个接口提供的主意,发送和吸收音信。

Socket简化了程序员操作,知道对方的IP和端口号的情景下,就能够给对方发送音讯,再有服务端来拍卖,由此必要服务端和客户端。

2.Socket的通讯进程

每三个选用只怕服务都有1个端口,因而须求包涵以下的步调:

  • 服务端利用Socket监听端口;

  • 客户端发起连接;

  • 服务端重返消息,建立连接,起头通讯;

  • 客户端,服务端断开连接;

TCP通讯的三回握手:叁遍握手(Three-way
Handshake),是指建立叁个TCP连接时,需求客户端和服务器总共发送一个包。

② 、各协议的界别

OSI模型把互连网通讯分成7层,由低向高分别是:物理层,数据链路层,互连网层,传输层,会话层,表示层和应用层。

咱俩常用的HTTP协议是对应应用层,TCP协议对应传输层,IP协议对应网络层,HTTP协议是依照TCP连接。

TCP/IP是传输层协议,首要解决数据如何在网络中传输;而HTTP是应用层协议,主要消除哪些包装数据。

在传输数据时候可以只是用TCP/IP,可是这么没有应用层,不可能识别传输的数码类容,那样是绝非意思的,如若想使传输的数额有意义,则必须利用应用层协议,HTTP正是一种,WEB使用它,封装HTTP文本消息,然后使用TCP/IP协议传输到互联网上

Socket实际上正是对TCP/IP协议的包裹,本身并不是协商,而是调用贰个接口(API),通过Socket,大家才能使用TCP/IP协议;

  1. 第一遍握手:客户端发送贰个TCP的SYN标志地点1的包指明客户打算连接的服务器的端口,以及起头序号X,保存在曲靖的种类号(Sequence
    Number)字段里。
  2. 首回握手:服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序号(Acknowledgement
    Number)设置为客户的I S N加1以.即X+1。
  3. 其三回握手:客户端再度发送确认包(ACK)
    SYN标志位为0,ACK标志位为1.并且把服务器发来ACK的序号字段+1,放在规定字段中发送给对方.并且在数据段放写ISN的+1

HTTP和Socket连接的不一致

1.TCP连接

Socket本人正是对TCP的卷入,就要先驾驭TCP连接:

确立三遍TCP连接需求举行”三回握手”:

tcp-3遍握手.png

第①领悟一下多少个标志,SYN(synchronous),同步标志,ACK
(Acknowledgement),即承认标志,seq应该是Sequence
Number,体系号的意趣,别的还有八遍握手的fin,应该是final,表示结束标志。

总结的用意大利语来表示正是:

客户端:hi,how are you?

服务端:fine,thank you,and you?

客户端:i am fine too.

  1. 客户端发送贰个TCP的SYN标志地点1的包指明客户打算连接的服务器的端口,以及发轫序号X,保存在淮安的体系号(Sequence
    Number)字段里。
  2. 服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将肯定序号(Acknowledgement
    Number)设置为客户的行列号加1以,即X+1。
  3. 客户端再次发送确认包(ACK)
    SYN标志位为0,ACK标志位为1。并且把服务器发来ACK的序号字段+1,放在规定字段中发送给对方.并且在数额段放写连串号的+1。

唯有拓展完叁次握手后,才能规范传输数据,理想图景下一旦建立起再三再四,在通讯双方积极关闭连接从前,TCP连接将会向来保持下去。二回握手可以确认保障对面已经接到自身的同步系列号,那样就能够有限支撑持续数据包的散失能够被察觉,那也是TCP流式传输的根底。

断开TCP连接必要发送多少个包,客户端和服务端都足以发起那些请求,在Socket编制程序中任何一方执行close()操作就会发生”肆次握手”:

tcp-8次握手.png

关门为何是4回,而一而再是3回,是因为当服务端收到客户端的SYN连接请求报文后,能够平素发送SYN+ACK报文,ACK用来答复,SYN用来一块。不过当关闭连接的气象下,接收端收到FIN报文时候,很只怕不会立即关闭,所以头阵送贰个ACK报布告诉发送端小编接到了,唯有等接收端报文全部发送完了,才能发送FIN报文。

2.HTTP连接

HTPP协议即超文本传送协议,是树立在TCP协议之上的一种。客户端每一遍发送的伏乞都要服务端回送响应,请求甘休后,会活动释放连接。

3.Socekt连接

概念:Socket是通讯的基础,是支撑TCP/IP协议的基本操作单元,蕴含5种消息:连接使用的商谈,本机主机IP地址,本地进度的端口号,远程主机IP地址,远程进度的协议端口。

应用层通过传输层实行数量传输时候,恐怕会遇上同3个TCP协议端口传输好两种多少,能够通过socket来区分分裂应用程序可能网络连接。

建立Socket连接的步骤

  1. 起码供给1对,三个效应于客户端,八个在服务端;
  2. 连天分为五个步骤:服务器监听,客户端请求,连接确认;
  3. 服务器监听:并不对应切切实实的客户端socket,而是处于等候连接情状,实时监听互联网状态,等待客户端连接;
  4. 客户端请求:客户端的套接字向劳动端套接字发起连接请求,因而要求驾驭服务端的套接字的地址和端口号,而且须求描述她要连接的服务器的套接字;
  5. 连天确认:当服务端套接字监听到也许接到到客户端的套接字的一连请求,就响应客户端的套接字,建立三个新的连天,把服务端的套接字的叙说发给客户端,一旦确认,双方就正式确立连接。而且服务端的套接字仍在监听状态,继续接受其余客户端的套接字。

Socket HTTP TCP区别

Socket连接能够钦命传输层协议,可以是TCP恐怕UDP,当时TCP协议时候正是二个TCP连接。而HTTP连接是伸手->响应的章程,在乞请时候必要先创立连接,然后客户端向服务器发出请求之后,服务器才能苏醒数据。而Socket一旦创造连接,服务器能够积极将数据传输给客户端;而HTTP则需求客户端先向服务器发送请求之后才能将数据重临给客户端。但实际Socket建立之后因为各样原因,会导致断开连接,其中3个缘故尽管防火墙会断开长日子处在非活跃状态的接连,由此供给轮询高速网络,那个两次三番是虎虎有生气的。

先看下服务端Socket监听代码:

叁 、在iOS里面包车型的士选取

iOS提供了Socket网络编制程序接口CFSocket,tcp和udp的socket是有分别的。

基于TCP的Socket:

tcp_Socket.png

基于UDP的Socket

udp_Socket.png

常用的Socket类型分为三种,流式Socket(SOCKET_STREAM)和数据报式(SOCKET_DGRAM),流式针对于面向TCP连接的选取,而数据报式是一种无连接的Socket,对应于无连接的UDP服务应用。

iOS官方给出的选择时CFSocket,它是基于BSD Socket举行抽象和包裹,CFSocket
中包括了个别支出,它差不多能够提供 BSD sockets 所负有的凡事成效,并且把
socket 集成进叁个“运营循环”个中。CFSocket 并不只限于基于流的 sockets
(比如 TCP),它能够处理其余类型的 socket。

你能够应用 CFSocketCreate 功能从头开端创建3个 CFSocket 对象,只怕应用
CFSocketCreateWithNative 函数从 BSD socket 创立。然后,需求运用函数
CFSocketCreateRunLoopSource
创造三个“运维循环”源,并运用函数CFRunLoopAddSource
把它加入3个“运转循环”。那样不管 CFSocket 对象是不是接收到音讯, CFSocket
回调函数都得以运作。


好了,废话少说,进入正题。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace SocketDome
{
    /// <summary>
    /// 处理Socket监听逻辑
    /// </summary>
    public class SocketProvider
    {
        private static Socket serviceSocketListener; //Socke监听处理请求

        /// <summary>
        /// 开启Socket监听
        /// </summary>
        public static void Init()
        {
            serviceSocketListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            serviceSocketListener.Bind(new IPEndPoint(IPAddress.Parse("10.0.0.217"), 20000)); //IP和端口应该是可配置
            serviceSocketListener.Listen(1024);
            Thread handleSocket = new Thread(new ThreadStart(HandleSocket));
            handleSocket.Start();
        }

        /// <summary>
        /// 监听链接
        /// </summary>
        private static void HandleSocket()
        {
            while (true)
            {
                try
                {
                    Socket currSocket = serviceSocketListener.Accept();  //为新建连接创建新的 System.Net.Sockets.Socket
                    Thread processThread = new Thread(new ParameterizedThreadStart(ProcessSocket));
                    processThread.Start(currSocket);
                }
                catch { }
            }
        }

        /// <summary>
        /// 处理Socket信息
        /// </summary>
        /// <param name="obj">新建连接创建新Socket对象</param>
        private static void ProcessSocket(object obj)
        {
            Socket currSocket = (Socket)obj;
            try
            {
                byte[] recvBytess = new byte[1048576];
                int recbytes;
                recbytes = currSocket.Receive(recvBytess, recvBytess.Length, 0);
                if (recbytes > 0)
                {
                    var contentStr = Encoding.UTF8.GetString(recvBytess, 0, recbytes);
                    var _order = contentStr.Split('~');
                    byte[] sendPass = Encoding.UTF8.GetBytes(_order[0].ToUpper() + "#SUCCESS"); //先相应对话,然后去异步处理
                    currSocket.Send(sendPass, sendPass.Length, SocketFlags.None);
                    switch (_order[0].ToUpper())
                    { 
                        case"ADDCACHE":
                            Console.WriteLine("添加缓存消息" + _order[1]);
                           //处理ADDCACHE逻辑
                           Console.WriteLine("写Log日志");
                            break;
                        default :
                            Console.WriteLine("命令错误");
                            Console.WriteLine("写Log日志");
                            break;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("写Error日志" + ex.Message);
            }
        }
    }
}

客户端

客户端成立Socket相对简单不少,步骤如下:

0.(可选)创立CFSocketContext->用来波及Socket上下文消息

  /*
  struct CFSocketContext
  {
      CFIndex version; 版本号,必须为0
      void *info; 一个指向任意程序定义数据的指针,可以在CFScocket对象刚创建的时候与之关联,被传递给所有在上下文中回调;
      CFAllocatorRetainCallBack retain; info指针中的retain回调,可以为NULL
      CFAllocatorReleaseCallBack release; info指针中的release的回调,可以为NULL
      CFAllocatorCopyDescriptionCallBack copyDescription; info指针中的回调描述,可以为NULL
  };
  typedef struct CFSocketContext CFSocketContext;
  */

实现代码:
  //这里把self作为数据指针传过去,这样在回调的时候就能拿到当前的VC
  CFSocketContext sockContext = {0,(__bridge void *)(self),NULL,NULL,NULL};

1.创建CFSocket对象

     CFSocketRef SocketRef = CFSocketCreate
    (
      //内存分配类型,一般为默认的Allocator->kCFAllocatorDefault
     <#CFAllocatorRef allocator#>,
      //协议族,一般为Ipv4:PF_INET,(Ipv6,PF_INET6)
     <#SInt32 protocolFamily#>,
     //套接字类型,TCP用流式—>SOCK_STREAM,UDP用报文式->SOCK_DGRAM
     <#SInt32 socketType#>,
     //套接字协议,如果之前用的是流式套接字类型:PPROTO_TCP,如果是报文式:IPPROTO_UDP
     <#SInt32 protocol#>,
     //回调事件触发类型 *1
     <#CFOptionFlags callBackTypes#>,
     //触发时候调用的方法 *2
     <#CFSocketCallBack callout#>,
     //用户定义的数据指针,用于对CFSocket对象的额外定义或者申明,可以为NULL
     <#const CFSocketContext *context#>
     );

*1 具体的回调事件触发类型
enum CFSocketCallBackType {
   kCFSocketNoCallBack = 0,
   kCFSocketReadCallBack = 1,
   kCFSocketAcceptCallBack = 2,(常用)
   kCFSocketDataCallBack = 3,
   kCFSocketConnectCallBack = 4,
   kCFSocketWriteCallBack = 8
};
typedef enum CFSocketCallBackType CFSocketCallBackType;

*2具体的触发调用的方法
CFSocketCallBack  在CFsocket对象中某个活跃类型被触发时候调用的触发函数
官方的申明是:typedef void (*CFSocketCallBack) ( CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info );也就是新建的这个方法要包含这些参数
/*!
 *  @brief socket回调函数
 *
 *  @param s            socket对象;
 *  @param callbackType 这个socket对象的活动类型;
 *  @param address      socket对象连接的远程地址,CFData对象对应的是socket对象中的protocol family(struct sockaddr_in 或者 struct sockaddr_in6), 除了type类型为kCFSocketAcceptCallBack和kCFSocketDataCallBack,否则这个值通常是NULL;
 *  @param data         跟回调类型相关的数据指针
    kCFSocketConnectCallBack:如果失败了,它指向的就是SINT32的错误代码;
    kCFSocketAcceptCallBack: 它指向的就是CFSocketNativeHandle
    kCFSocketDataCallBack:   它指向的就是将要进来的Data;
    其他情况都是NULL
 *  @param info         与Socket相关的自定义的任意数据
 */

实现代码:
  _socketRef = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketConnectCallBack,ServerConnectCallBack, &sockContext);

2.开立Socket供给接二连三的地方,这是二个结构体,须求包罗多少个参数,同事IPV4和IPV6不均等

  // ----创建sockadd_in的结构体,该结构体作为socket的地址,IPV6需要改参数
  struct sockaddr_in addr;

  //创建完结构体先把这个结构体进行清零操作
  //memset:将addr中所有字节用0替换并返回addr,作用是一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
  memset(&addr, 0, sizeof(addr));

 /* 设置addr的具体内容
  struct sockaddr_in {
  __uint8_t sin_len; 长度
  sa_family_t   sin_family;  协议族,用AF_INET->互联网络,TCP,UDP等等
  in_port_t sin_port;    端口号(使用网络字节顺序) htons:将主机的无符号短整形数转换成网络字节顺序
  struct    in_addr sin_addr; 存储IP地址,使用inet_addr()这个函数,用来将一个点分十进制的IP转换成一个长整数型数(u_long类型),若字符串有效则将字符串转换为32位二进制网络字节序的IPV4地址,否则为INADDR_NONE
  char      sin_zero[8]; 让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节,无需处理
         };*/

  addr.sin_len = sizeof(addr);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(19992);
  addr.sin_addr.s_addr = inet_addr(192.168.1.333);

3.把地址转换来CFDataRef

  CFDataRef dataRef = CFDataCreate(kCFAllocatorDefault,(UInt8 *)&addr, sizeof(addr));

4.连接
那边连接有2种方案:

  • 方案一:

若是上边SocketRef创造爱您时候选用回调类型为kCFSocketNoCallBack,然后没有安装回调函数,那就一贯开始展览延续

  /*!
   *  @brief 连接socket
   *
   *  @param s       连接的socket
   *  @param address 连接的socket的包含的地址参数
   *  @param timeout 连接超时时间,如果为负,则不尝试连接,而是把连接放在后台进行,如果_socket消息类型为kCFSocketConnectCallBack,将会在连接成功或失败的时候在后台触发回调函数
   *
   *  @return        返回CFSocketError类型

  CFSocketConnectToAddress(CFSocketRef s, CFDataRef address, CFTimeInterval timeout)
  */

  CFSocketError result = CFSocketConnectToAddress(_socketRef, dataRef, 5);

  /*
  typedef CF_ENUM(CFIndex, CFSocketError) {
    kCFSocketSuccess = 0,成功
    kCFSocketError = -1L,失败
    kCFSocketTimeout = -2L 超时
};
*/

方案二:

若果设置回调参数为kCFSocketConnectCallBack,并且安装了回调函数

  // ----连接
  CFSocketConnectToAddress(_socketRef, dataRef, -1);

  // ----加入循环中

  // ----获取当前线程的RunLoop
  CFRunLoopRef runLoopRef = CFRunLoopGetCurrent();

  // ----把Socket包装成CFRunLoopSource,最后一个参数是指有多个runloopsource通过同一个runloop时候顺序,如果只有一个source通常为0
  CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socketRef, 0);

  // ----加入运行循环,第三个参数表示
  CFRunLoopAddSource(runLoopRef, //运行循环管
                     sourceRef, // 增加的运行循环源, 它会被retain一次
                     kCFRunLoopCommonModes //用什么模式把source加入到run loop里面,使用kCFRunLoopCommonModes可以监视所有通常模式添加source
                     );

  CFRelease(sourceRef);

5.连接成功和波折的判定

设若方案一:

  if (result == kCFSocketSuccess) {

        // ----另外一个线程读取数据
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self readStreamData];
        });
    }    

假设方案二:

void ServerConnectCallBack ( CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info )
{
  //这里是当前控制器名字,从info中取得之前存储的控制器,然后在后台执行刷新数据方法
  ViewController *vc = (__bridge ViewController *)(info);
  ViewController *vc = (__bridge ViewController *)(info);

  // ----判断是不是NULL
  if (data != NULL) {
      printf("连接失败\n");
     [vc performSelector:@selector(releaseSocket) withObject:nil];
  }else {
      printf("连接成功\n");
      [vc performSelectorInBackground:@selector(readStreamData) withObject:nil];
  }
}

6.读取数据

- (void)readStreamData
{
  // ----定义一个字符型变量
  char buffer[512];

  /** 
      int recv( SOCKET s, char FAR *buf, int len, int flags );

   不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。

   (1)第一个参数指定接收端套接字描述符;

   (2)第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;

   (3)第三个参数指明buf的长度;

   (4)第四个参数一般置0。

   */

  long readData;
  //若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。如果发生错误,返回-1,应用程序可通过perror()获取相应错误信息
  while((readData = recv(CFSocketGetNative(_socketRef), buffer, sizeof(buffer), 0))) {

      NSString *content = [[NSString alloc] initWithBytes:buffer length:readData encoding:NSUTF8StringEncoding];

      dispatch_async(dispatch_get_main_queue(), ^{

           self.infoLabel.text = [NSString stringWithFormat:@"%@\n%@",content,self.infoLabel.text];

      });
  }
  perror("recv");
}

7.向服务端上传数据

  NSString *stringTosend = [NSString stringWithFormat:@"%@说:%@",self.nameText.text,self.messageText.text];

  const char* data = [stringTosend UTF8String];

  /** 成功则返回实际传送出去的字符数, 失败返回-1. 错误原因存于errno*/
  int sendData = send(CFSocketGetNative(_socketRef), data, strlen(data) + 1, 0);

  if (sendData < 0) {
      perror("send");
  }

这么些服务端,监听着客户端发来的下令,格式定义为:命令~参数,在服务端接受到客户端的授命新闻后随即回传接到命令并初始拍卖,进行异步处理防止客户端等待。

服务端

服务端创造Socket相对复杂一下;

  1. 创建Socket对象

  //同客户端,注释就不写了
  CFSocketRef _socket = CFSocketCreate(kCFAllocatorDefault,
                                           PF_INET,
                                           SOCK_STREAM,
                                           IPPROTO_TCP ,
                                           kCFSocketAcceptCallBack,
                                           TCPServerAcceptCallBack,
                                           NULL);

  if (_socket == NULL) {
    NSLog(@"创建Socket失败!");
    return;
  }
  1. 安装允许重用本地地址和端口
    此处首先介绍下setsockopt()这些函数,它是用来安装Socket关联的选项,选项大概存在于多层协商业中学,它们总会出现在最上边的套接字层。当操作套接字选项时,选项位于的层和选拔的称呼必须交给。为了操作套接字层的选项,应该
    将层的值钦定为SOL_SOCKET。为了操作别的层的选项,控制选项的确切协议号必须付出。

int setsockopt(int sock,  //需要设置选项的套接字
               int level, //选项所在的协议层
               int optname, //需要访问的选项名
               const void *optval, //新选项值的缓冲
               socklen_t optlen //现选项的长度
               );

/* 成功返回0,失败返回-1。
失败的errno:
EBADF:sock不是有效的文件描述词
EFAULT:optval指向的内存并非有效的进程空间
EINVAL:在调用setsockopt()时,optlen无效
ENOPROTOOPT:指定的协议层不能识别选项
ENOTSOCK:sock描述的不是套接字
*/

/*
参数的详细说明
1. level指定控制套接字的层次.可以取三种值:
1)SOL_SOCKET:通用套接字选项.(常用)
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项. 

2. optname指定控制的方式(选项的名称)
 SO_REUSERADDR- 允许重用本地地址和端口- int
3.optval设置套接字选项.根据选项名称的数据类型进行转换,这里是int类型,我把它用BOOL来代替,1表示YES,0表示NO;
4. optlen 是指上面optval长度
*/

具体的代码:

  BOOL reused = YES;
  //设置允许重用本地地址和端口
  setsockopt(CFSocketGetNative(_socket), SOL_SOCKET, SO_REUSEADDR, (const void *)&reused, sizeof(reused));

3.创设Socket须求一而再的地方

  //定义sockaddr_in类型的变量,该变量将作为CFSocket的地址
  struct sockaddr_in Socketaddr;
  memset(&Socketaddr, 0, sizeof(Socketaddr));
  Socketaddr.sin_len = sizeof(Socketaddr);
  Socketaddr.sin_family = AF_INET;
  //设置该服务器监听本机任意可用的IP地址
  //                addr4.sin_addr.s_addr = htonl(INADDR_ANY);
  //设置服务器监听地址
  Socketaddr.sin_addr.s_addr = inet_addr(TEST_IP_ADDR);
  //设置服务器监听端口
  Socketaddr.sin_port = htons(TEST_IP_PROT);

4.转移地址类型,连接

  //将IPv4的地址转换为CFDataRef
  CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8 *)&Socketaddr, sizeof(Socketaddr));

  //将CFSocket绑定到指定IP地址
  if (CFSocketSetAddress(_socket, address) != kCFSocketSuccess) {
      //如果_socket不为NULL,则释放_socket
      if (_socket) {
          CFRelease(_socket);
          exit(1);
      }
      _socket = NULL;
  }

5.出席RunLoop循环监听

  NSLog(@"----启动循环监听客户端连接---");
  //获取当前线程的CFRunLoop
  CFRunLoopRef cfRunLoop = CFRunLoopGetCurrent();
  //将_socket包装成CFRunLoopSource
  CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0);
  //为CFRunLoop对象添加source
  CFRunLoopAddSource(cfRunLoop, source, kCFRunLoopCommonModes);
  CFRelease(source);
  //运行当前线程的CFRunLoop
  CFRunLoopRun();

6.回调函数

//有客户端连接进来的回调函数
void TCPServerAcceptCallBack(CFSocketRef socket,
                             CFSocketCallBackType type,
                             CFDataRef address,
                             const void *data,
                             void *info)
{
    //如果有客户端Socket连接进来
    if (kCFSocketAcceptCallBack == type) {

        //获取本地Socket的Handle,这个回调事件的类型是kCFSocketAcceptCallBack,这个data就是一个CFSocketNativeHandle类型指针
        CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;

        //定义一个255数组接收这个新的data转成的socket的地址,SOCK_MAXADDRLEN意思是最长的可能的地址
        uint8_t name[SOCK_MAXADDRLEN];
        //这个地址数组的长度
        socklen_t namelen = sizeof(name);

        /**
            int getpeername(int,已经连接的Socket
                            struct sockaddr * __restrict,用来接收地址信息
                            socklen_t * __restrict 地址长度
                            )
         作用是从已经连接的Socket中获得地址信息,存到参数2中,地址长度放到参数3中

         成功是返回0,如果失败了则返回别的数字,对应不同错误码

         */
        //获取Socket信息
        if (getpeername(nativeSocketHandle,
                        (struct sockaddr *)name,
                        &namelen) != 0 ) {

            perror("getpeername:");
            exit(1);
        }

        //获取连接信息
        struct sockaddr_in *addr_in = (struct sockaddr_in *)name;
        // ----inet_ntoa将网络地址转换成“.”点隔的字符串格式
        NSLog(@"%s:%d连接进来了",inet_ntoa(addr_in->sin_addr),addr_in->sin_port);

        //创建一组可读/写的CFStream
        readStreamRef  = NULL;
        writeStreamRef = NULL;

        // ----创建一个和Socket对象相关联的读取数据流
        CFStreamCreatePairWithSocket(kCFAllocatorDefault, //内存分配器
                                     nativeSocketHandle, //准备使用输入输出流的socket
                                     &readStreamRef, //输入流
                                     &writeStreamRef);//输出流

        // ----CFStreamCreatePairWithSocket()操作成功后,readStreamRef和writeStreamRef都指向有效的地址,因此判断是不是还是之前设置的NULL就可以了
        if (readStreamRef && writeStreamRef) {

            //打开输入流和输出流
            CFReadStreamOpen(readStreamRef);
            CFWriteStreamOpen(writeStreamRef);

            // ----一个结构体包含程序定义数据和回调用来配置客户端数据流行为
            NSString *aaa = @"earth,wind,fire,be my call";

            CFStreamClientContext context = {0,(__bridge void *)(aaa),NULL,NULL};

            /** 
             指定客户端的数据流,当特定事件发生的时候,接受回调
             Boolean CFReadStreamSetClient ( CFReadStreamRef stream, 需要指定的数据流
                                             CFOptionFlags streamEvents, 具体的事件,如果为NULL,当前客户端数据流就会被移除
                                             CFReadStreamClientCallBack clientCB, 事件发生回调函数,如果为NULL,同上
                                             CFStreamClientContext *clientContext 一个为客户端数据流保存上下文信息的结构体,为NULL同上
                                            );
             返回值为TRUE就是数据流支持异步通知,FALSE就是不支持
             */
            if (!CFReadStreamSetClient(readStreamRef,
                                       kCFStreamEventHasBytesAvailable,
                                       readStream,
                                       &context)) {
                exit(1);
            }

            // ----将数据流加入循环
            CFReadStreamScheduleWithRunLoop(readStreamRef,
                                            CFRunLoopGetCurrent(),
                                            kCFRunLoopCommonModes);

            const char *str = "welcome!\n";

            //向客户端输出数据
            CFWriteStreamWrite(writeStreamRef, (UInt8 *)str, strlen(str) + 1);

        }else {
            // ----如果失败就销毁已经连接的Socket
            close(nativeSocketHandle);
        }   
    } 
}

7.读取客户端发来的多寡

void readStream(CFReadStreamRef readStream,
                CFStreamEventType evenType,
                void *clientCallBackInfo)
{
    UInt8 buff[2048];

    NSString *aaa = (__bridge NSString *)(clientCallBackInfo);

    NSLog(@"%@", aaa);

    // ----从可读的数据流中读取数据,返回值是多少字节读到的,如果为0就是已经全部结束完毕,如果是-1则是数据流没有打开或者其他错误发生
    CFIndex hasRead = CFReadStreamRead(readStream, buff, sizeof(buff));

    if (hasRead > 0) {
        printf("接收到数据:%s\n",buff);

        const char *str = "for the lich king!!\n";
        //向客户端输出数据
        CFWriteStreamWrite(writeStreamRef, (UInt8 *)str, strlen(str) + 1);
    }
}

源码地址:https://github.com/medivh-xiong/CFSocket\_Demo.git

下边看下客户端的Socket客户端主动请求服务端代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace ConsoleApplication7
{
    /// <summary>
    /// Socket Helper
    /// </summary>
    public class SocketHelper
    {
        private string ip;
        private IPEndPoint ex;
        private Socket socket;

        public SocketHelper(string ip, int port)
        {
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            this.ip = ip;
            this.ex = new IPEndPoint(IPAddress.Parse(ip), port);
        }

        /// <summary>
        /// Socket 进行连接
        /// </summary>
        /// <returns>连接失败OR成功</returns>
        public bool Socketlink()
        {
            try
            {
                socket.Connect(ex);
                return true;
            }
            catch (Exception ex)
            {
                return false;
            }
        }

        /// <summary>
        /// Socket 发送消息
        /// </summary>
        /// <param name="strmsg">消息</param>
        public void SendVarMessage(string strmsg)
        {
            try
            {
                byte[] msg = System.Text.Encoding.UTF8.GetBytes(strmsg);
                this.socket.Send(msg);
            }
            catch (Exception ex)
            {
                this.socket.Close();
            }
        }

        /// <summary>
        /// Socket 消息回传
        /// </summary>
        /// <returns></returns>
        public string ReceiveMessage()
        {
            try
            {
                byte[] msg = new byte[1048576];
                int recv = socket.Receive(msg);
                this.socket.Close();
                return System.Text.Encoding.UTF8.GetString(msg, 0, recv);
            }
            catch (Exception ex)
            {
                this.socket.Close();
                return "ERROR";
            }
        }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication7
{
    class Program
    {
        static void Main(string[] args)
        {
            SocketHelper socket = new SocketHelper("10.0.0.217",20000);
            if(socket.Socketlink())
            {
                Console.WriteLine("连接成功");
                socket.SendVarMessage("ADDCACHE~张三");
                string strReposon = socket.ReceiveMessage();
                Console.WriteLine(strReposon);
            }
            Console.Read();
        }
    }
}

首先以管理园身份开启服务端查询,然后客户端主动请求服务端举办音信请求。

 

图片 1