iOS 用OS底层scoket实现了简单的IM

我们客户端的实现思路也是很简单,创建Socket,和服务器的Socket对接上,然后开始传输数据就可以了。
我们学c/c++或者java这些语言,我们就知道,往往任何教程,最后一章都是讲Socket编程,而Socket是什么呢,简单的来说,就是我们使用TCP/IP 或者UDP/IP协议的一组编程接口。如下图所示:

socket

我们在应用层,使用socket,轻易的实现了进程之间的通信(跨网络的)。想想,如果没有socket,我们要直面TCP/IP协议,我们需要去写多少繁琐而又重复的代码。

如果有对socket概念仍然有所困惑的,可以看看这篇文章:
从问题看本质,socket到底是什么?
但是这篇文章关于并发连接数的认识是错误的,正确的认识可以看看这篇文章:
单台服务器并发TCP连接数到底可以有多少

我们接着可以开始着手去实现IM了,首先我们不基于任何框架,直接去调用OS底层-基于C的BSD Socket去实现,它提供了这样一组接口:

//socket 创建并初始化 socket,返回该 socket 的文件描述符,如果描述符为 -1 表示创建失败。
int socket(int addressFamily, int type,int protocol)
//关闭socket连接
int close(int socketFileDescriptor)
//将 socket 与特定主机地址与端口号绑定,成功绑定返回0,失败返回 -1。
int bind(int socketFileDescriptor,sockaddr *addressToBind,int addressStructLength)
//接受客户端连接请求并将客户端的网络地址信息保存到 clientAddress 中。
int accept(int socketFileDescriptor,sockaddr *clientAddress, int clientAddressStructLength)
//客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。
int connect(int socketFileDescriptor,sockaddr *serverAddress, int serverAddressLength)
//使用 DNS 查找特定主机名字对应的 IP 地址。如果找不到对应的 IP 地址则返回 NULL。
hostent* gethostbyname(char *hostname)
//通过 socket 发送数据,发送成功返回成功发送的字节数,否则返回 -1。
int send(int socketFileDescriptor, char *buffer, int bufferLength, int flags)
//从 socket 中读取数据,读取成功返回成功读取的字节数,否则返回 -1。
int receive(int socketFileDescriptor,char *buffer, int bufferLength, int flags)
//通过UDP socket 发送数据到特定的网络地址,发送成功返回成功发送的字节数,否则返回 -1。
int sendto(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *destinationAddress, int destinationAddressLength)
//从UDP socket 中读取数据,并保存发送者的网络地址信息,读取成功返回成功读取的字节数,否则返回 -1 。
int recvfrom(int socketFileDescriptor,char *buffer, int bufferLength, int flags, sockaddr *fromAddress, int *fromAddressLength)

客户端

让我们可以对socket进行各种操作,首先我们来用它写个客户端。总结一下,简单的IM客户端需要做如下4件事:

  1. 客户端调用 socket(...) 创建socket;
  2. 客户端调用 connect(...) 向服务器发起连接请求以建立连接;
  3. 客户端与服务器建立连接之后,就可以通过send(...)/receive(...)向客户端发送或从客户端接收数据;
  4. 客户端调用 close 关闭 socket;

根据上面4条大纲,我们封装了一个名为TYHSocketManager的单例,来对socket相关方法进行调用:

YHSocketManager.h

#import <Foundation/Foundation.h>

@interface TYHSocketManager : NSObject
+ (instancetype)share;
- (void)connect;
- (void)disConnect;
- (void)sendMsg:(NSString *)msg;
@end

YHSocketManager.m

#import "TYHSocketManager.h"

#import <sys/types.h>
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>

@interface TYHSocketManager()

@property (nonatomic,assign)int clientScoket;

@end

@implementation TYHSocketManager

+ (instancetype)share
{
    static dispatch_once_t onceToken;
    static TYHSocketManager *instance = nil;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc]init];
        [instance initScoket];
        [instance pullMsg];
    });
    return instance;
}

- (void)initScoket
{
    //每次连接前,先断开连接
    if (_clientScoket != 0) {
        [self disConnect];
        _clientScoket = 0;
    }
   
    //创建客户端socket
    _clientScoket = CreateClinetSocket();
  
    //服务器Ip
    const char * server_ip="127.0.0.1";
    //服务器端口
    short server_port=6969;
    //等于0说明连接失败
    if (ConnectionToServer(_clientScoket,server_ip, server_port)==0) {
        printf("Connect to server error\n");
        return ;
    }
    //走到这说明连接成功
    printf("Connect to server ok\n");
}

static int CreateClinetSocket()
{
    int ClinetSocket = 0;
    //创建一个socket,返回值为Int。(注scoket其实就是Int类型)
    //第一个参数addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。
    //第二个参数 type 表示 socket 的类型,通常是流stream(SOCK_STREAM) 或数据报文datagram(SOCK_DGRAM)
    //第三个参数 protocol 参数通常设置为0,以便让系统自动为选择我们合适的协议,对于 stream socket 来说会是 TCP 协议(IPPROTO_TCP),而对于 datagram来说会是 UDP 协议(IPPROTO_UDP)。
    ClinetSocket = socket(AF_INET, SOCK_STREAM, 0);
    return ClinetSocket;
}
static int ConnectionToServer(int client_socket,const char * server_ip,unsigned short port)
{
    
    //生成一个sockaddr_in类型结构体
    struct sockaddr_in sAddr={0};
    sAddr.sin_len=sizeof(sAddr);
    //设置IPv4
    sAddr.sin_family=AF_INET;
    
    //inet_aton是一个改进的方法来将一个字符串IP地址转换为一个32位的网络序列IP地址
    //如果这个函数成功,函数的返回值非零,如果输入地址不正确则会返回零。
    inet_aton(server_ip, &sAddr.sin_addr);
    
    //htons是将整型变量从主机字节顺序转变成网络字节顺序,赋值端口号
    sAddr.sin_port=htons(port);
    
    //用scoket和服务端地址,发起连接。
    //客户端向特定网络地址的服务器发送连接请求,连接成功返回0,失败返回 -1。
    //注意:该接口调用会阻塞当前线程,直到服务器返回。
    if (connect(client_socket, (struct sockaddr *)&sAddr, sizeof(sAddr))==0) {
        return client_socket;
    }
    return 0;
}

#pragma mark - 新线程来接收消息

- (void)pullMsg
{
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(recieveAction) object:nil];
    [thread start];
}

#pragma mark - 对外逻辑

- (void)connect
{
    [self initScoket];
}
- (void)disConnect
{
    //关闭连接
    close(self.clientScoket);
}

//发送消息
- (void)sendMsg:(NSString *)msg
{
    
    const char *send_Message = [msg UTF8String];
    send(self.clientScoket,send_Message,strlen(send_Message)+1,0);
    
}

//收取服务端发送的消息
- (void)recieveAction{
    while (1) {
        char recv_Message[1024] = {0};
        recv(self.clientScoket, recv_Message, sizeof(recv_Message), 0);
        printf("%s\n",recv_Message);
    }
}

如上所示:

我们调用了initScoket方法,利用CreateClinetSocket方法了一个scoket,就是就是调用了socket函数:
ClinetSocket = socket(AF_INET, SOCK_STREAM, 0);
然后调用了ConnectionToServer函数与服务器连接,IP地址为127.0.0.1也就是本机localhost和端口6969相连。在该函数中,我们绑定了一个sockaddr_in类型的结构体,该结构体内容如下:
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
里面包含了一些,我们需要连接的服务端的scoket的一些基本参数,具体赋值细节可以见注释。

连接成功之后,我们就可以调用send函数和recv函数进行消息收发了,在这里,我新开辟了一个常驻线程,在这个线程中一个死循环里去不停的调用recv函数,这样服务端有消息发送过来,第一时间便能被接收到。
就这样客户端便简单的可以用了,接着我们来看看服务端的实现。

服务器端

一样,我们首先对服务端需要做的工作简单的总结下:

  1. 服务器调用 socket(...) 创建socket;
  2. 服务器调用 listen(...) 设置缓冲区;
  3. 服务器通过 accept(...)接受客户端请求建立连接;
  4. 服务器与客户端建立连接之后,就可以通过 send(...)/receive(...)向客户端发送或从客户端接收数据;
  5. 服务器调用 close 关闭 socket;
接着我们就可以具体去实现了

OS底层的函数是支持我们去实现服务端的,但是我们一般不会用iOS去这么做(试问真正的应用场景,有谁用iOSscoket服务器么...),如果还是想用这些函数去实现服务端,可以参考下这篇文章: 深入浅出Cocoa-iOS网络编程之Socket

node.js服务器实现
var net = require('net');  
var HOST = '127.0.0.1';  
var PORT = 6969;  
  
// 创建一个TCP服务器实例,调用listen函数开始监听指定端口  
// 传入net.createServer()的回调函数将作为”connection“事件的处理函数  
// 在每一个“connection”事件中,该回调函数接收到的socket对象是唯一的  
net.createServer(function(sock) {  
  
    // 我们获得一个连接 - 该连接自动关联一个socket对象  
    console.log('CONNECTED: ' +  
        sock.remoteAddress + ':' + sock.remotePort);  
        sock.write('服务端发出:连接成功');  

    // 为这个socket实例添加一个"data"事件处理函数  
    sock.on('data', function(data) {  
        console.log('DATA ' + sock.remoteAddress + ': ' + data);  
        // 回发该数据,客户端将收到来自服务端的数据  
        sock.write('You said "' + data + '"');  
    });  
    // 为这个socket实例添加一个"close"事件处理函数  
    sock.on('close', function(data) {  
        console.log('CLOSED: ' +  
        sock.remoteAddress + ' ' + sock.remotePort);  
    });  
  
}).listen(PORT, HOST);  
  
console.log('Server listening on ' + HOST +':'+ PORT);  

服务器运行起来了,并且监听着6969端口。
接着我们用之前写的iOS端的例子。客户端打印显示连接成功,而我们运行的服务器也打印了连接成功。接着我们发了一条消息,服务端成功的接收到了消息后,把该消息再发送回客户端,绕了一圈客户端又收到了这条消息。至此我们用OS底层scoket实现了简单的IM。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容