我们客户端的实现思路也是很简单,创建Socket,和服务器的Socket对接上,然后开始传输数据就可以了。
我们学c/c++
或者java
这些语言,我们就知道,往往任何教程,最后一章都是讲Socket
编程,而Socket是什么呢,简单的来说,就是我们使用TCP/IP 或者UDP/IP协议的一组编程接口。如下图所示:
我们在应用层,使用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件事:
- 客户端调用 socket(...) 创建socket;
- 客户端调用 connect(...) 向服务器发起连接请求以建立连接;
- 客户端与服务器建立连接之后,就可以通过send(...)/receive(...)向客户端发送或从客户端接收数据;
- 客户端调用 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函数,这样服务端有消息发送过来,第一时间便能被接收到。
就这样客户端便简单的可以用了,接着我们来看看服务端的实现。
服务器端
一样,我们首先对服务端需要做的工作简单的总结下:
- 服务器调用 socket(...) 创建socket;
- 服务器调用 listen(...) 设置缓冲区;
- 服务器通过 accept(...)接受客户端请求建立连接;
- 服务器与客户端建立连接之后,就可以通过 send(...)/receive(...)向客户端发送或从客户端接收数据;
- 服务器调用 close 关闭 socket;
接着我们就可以具体去实现了
OS
底层的函数是支持我们去实现服务端的,但是我们一般不会用iOS
去这么做(试问真正的应用场景,有谁用iOS
做scoket
服务器么...),如果还是想用这些函数去实现服务端,可以参考下这篇文章: 深入浅出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。