码迷,mamicode.com
首页 > 其他好文 > 详细

Socket编程(简易聊天室客户端/服务器编写、CocoaAsyncSocket)

时间:2016-07-14 15:23:09      阅读:497      评论:0      收藏:0      [点我收藏+]

标签:

Socket编程(简易聊天室客户端/服务器编写、CocoaAsyncSocket)

一、Socket

1.1 Socket简介

Socket就是为网络服务提供的一种机制。网络通信其实就是Socket间的通信,通信的两端都是Socket,数据在两个Socket间通过IO传输。

技术分享

在Web服务大行其道的今天,调用Web服务的代价是高昂的,尤其是仅仅是抓取少量数据的时候尤其如此。而使用Socket,可以只传送数据本身而不用进行XML封装,大大降低数据传输的开销。Socket允许使用长连接,允许应用程序运行在异步模式(提高效率),只有在需要的时候才接收数据

1.2 模仿QQ通信流程

技术分享

1.3 socket通信流程图

技术分享

1.4 Socket连接

iOS中常用的两种Socket类型:

  1. 流式Socket(SOCK_STREAM):流式是一种面向连接的Socket,针对于面向连接的TCP服务应用
  2. 数据报式Socket(SOCK_DGRAM):数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用

在iOS中使用流式Socket连接的方法:

  1. 以NSStream(流)的方式来发送和接收数据
  2. 设置流的代理,对流状态的变化做出相应
    a. 连接建立
    b. 接收到数据
    c. 连接关闭

其中:
1. NSStream:数据流的父类,用于定义抽象特性,例如:打开、关闭代理,继承自CFStream(Core Foundation)
2. NSInputStream:NSStream的子类,用于读取输入
3. NSOutputStream:NSSTream的子类,用于写输出

二、简单的聊天室编写

2.1 程序目标

本程序完成聊天室的连接、登录、发送/接收数据三个功能。编写本程序之前,首先需要一个后台文件用于开启聊天室服务,端口为12345,并规定使用”iam:xxx”的指令登录一个名为”xxx”的用户,使用”msg:xxx”的指令发送一条内容为”xxx”的数据。

程序界面:
技术分享

2.2 弹出键盘

#import "ViewController.h"

@interface ViewController () <NSStreamDelegate, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>{
    NSInputStream *_inputStream;
    NSOutputStream *_outputStream;
}

- (IBAction)connectToHost:(id)sender;
- (IBAction)login:(id)sender;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint;
@property (weak, nonatomic) IBOutlet UITableView *tableView;

@property (nonatomic, strong) NSMutableArray *msgs;

@end

@implementation ViewController

- (NSMutableArray *)msgs
{
    if (_msgs == nil) {
        _msgs = [NSMutableArray array];
    }
    return _msgs;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kbWillShow:) name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kbWillHide:) name:UIKeyboardWillHideNotification object:nil];

}

#pragma mark 键盘即将显示
- (void)kbWillShow:(NSNotification *)noti
{
//    NSLog(@"%@", noti.userInfo);
    CGFloat kbHeight = [noti.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;

    self.bottomConstraint.constant = kbHeight;
}

#pragma mark 键盘即将隐藏
- (void)kbWillHide:(NSNotification *)noti
{
    self.bottomConstraint.constant = 0;
}

#pragma mark 退出键盘
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    [self.view endEditing:YES];
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

2.3 连接服务器、登录

#pragma mark 连接服务器
- (IBAction)connectToHost:(id)sender
{
    //iOS中需要使用C语言实现Socket的连接

    //1.设置主机ip、端口地址
    NSString *host = @"127.0.0.1";
    int port = 12345;

    //2.定义输入输出流
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;

    //3.分配输入输出流的内存空间
    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream);

    //4.把C语言对象转换为OC对象
    _inputStream = (__bridge NSInputStream *)readStream;
    _outputStream = (__bridge NSOutputStream *)writeStream;

    //5.设置代理,监听数据接受的状态
    _inputStream.delegate = self;
    _outputStream.delegate = self;

    //6.把输入输出流添加到主运行循环,主运行循环用于监听网络状态
    [_inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    [_outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

    //7.打开输入输出流
    [_inputStream open];
    [_outputStream open];
}

#pragma mark 登录
- (IBAction)login:(id)sender
{
    NSString *loginStr = @"iam:zhangsan";

    [self sendDataToHost:loginStr];
}

#pragma mark NSStream代理方法
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    switch (eventCode) {

        case NSStreamEventOpenCompleted:
            NSLog(@"%@", aStream);
            NSLog(@"成功建立连接");
            break;

        case NSStreamEventHasBytesAvailable:
            NSLog(@"有数据可读");
            [self readData];
            break;

        case NSStreamEventHasSpaceAvailable:
            NSLog(@"可以发送数据");
            break;
        case NSStreamEventErrorOccurred:
            NSLog(@"出现错误");
            break;

        case NSStreamEventEndEncountered:
            NSLog(@"正常地中断连接");
            //断开连接后要记得关闭输入输出流,并从主运行循环中移除
            [_inputStream close];
            [_outputStream close];
            [_inputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
            [_outputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
            break;

        default:
            break;
    }

}

2.4 发送、读取数据

#pragma mark 读取数据
- (void)readData
{
    //定义缓冲区,这个缓冲区只能存储1024个字节
    uint8_t buf[1024];

    //len为读取到地实际字节数
    NSInteger len = [_inputStream read:buf maxLength:sizeof(buf)];

    //把缓冲区里地字节转换为字符串
    NSString *receiveStr = [[NSString alloc] initWithBytes:buf length:len encoding:NSUTF8StringEncoding];
    NSLog(@"%@", receiveStr);

    [self.msgs addObject:receiveStr];
    [self.tableView reloadData];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.msgs.count;
}

#pragma mark 设置tableView数据
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *ID = @"msg";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:ID];
    }

    cell.textLabel.text = self.msgs[indexPath.row];

    return cell;
}

#pragma mark 按下发送键
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    NSString *str = textField.text;

    if (str.length == 0) {
        return YES;
    }

    NSString *msg = [@"msg:" stringByAppendingString:str];
    [self sendDataToHost:msg];

    textField.text = nil;

    return YES;
}

#pragma mark 发送数据
- (void)sendDataToHost:(NSString *)str
{
    NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
    [_outputStream write:data.bytes maxLength:data.length];
}

三、第三方框架(CocoaAsyncSocket)

3.1 CocoaAsyncSocket

Socket的使用都是基于C语言的,不便于开发和维护,可以第三方框架CocoaAsyncSocket编写本程序。CocoaAsyncSocket支持TCP和UDP,其中,AsyncSocket类是支持TCP的,AsyncUdpSocket是支持UDP的。可根据需要导入相关的文件,本文使用的是AsyncSocket。AsyncSocket是封装了CFSocket和CFSteam的TCP/IP socket网络库,提供异步操作,它的代理方法都是在子线程下进行的。

3.2 程序改编

上面的程序使用CocoaAsyncSocket实现,使用前导入GCDAsyncSocket.h,并新建成员变量GCDAsyncSocket *_socket,之后:

#pragma mark socket代理方法
#pragma mark 连接服务器
- (IBAction)connectToHost:(id)sender
{
    //iOS中需要使用C语言实现Socket的连接

    //1.设置主机ip、端口地址
    NSString *host = @"127.0.0.1";
    int port = 12345;

    _socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];

    NSError *error = nil;
    [_socket connectToHost:host onPort:port error:&error];
    if (error) {
        NSLog(@"连接失败%@", error);
    }
}

#pragma mark 连接成功
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
    NSLog(@"连接成功");
}

#pragma mark 断开连接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
    if (err) {
        NSLog(@"出现错误,连接失败");
    }else {
        NSLog(@"断开连接");
    }
}

#pragma mark 数据发送成功
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
    [sock readDataWithTimeout:-1 tag:tag];
}

#pragma mark 读取数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
    NSString *receiveStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    if (tag==101) { //登录
        NSLog(@"登录成功");
    }else if (tag==102) {//发送数据

        [self.msgs addObject:receiveStr];

        dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadData];
        });

    }
}

#pragma mark 按下登录按钮
- (IBAction)login:(id)sender
{
    NSString *loginStr = @"iam:zhangsan";

    [_socket writeData:[loginStr dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:101];
}

UITextField按下发送键,发送数据:

[_socket writeData:[msg dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:102];

四、聊天室服务端编写

4.1 编写服务端

#import "ChatServer.h"
#import "GCDAsyncSocket.h"

@interface ChatServer () <GCDAsyncSocketDelegate>{
    GCDAsyncSocket *_serverSocket;
}

/**
 *  所有客户端连接的Socket
 */
@property (nonatomic, strong) NSMutableArray *clientSocket;


@end

@implementation ChatServer

- (NSMutableArray *)clientSocket
{
    if (_clientSocket == nil) {
        _clientSocket = [NSMutableArray array];
    }

    return _clientSocket;
}

- (instancetype)init
{
    if (self = [super init]) {
        _serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
    }
    return self;
}

#pragma mark 公共方法,开启服务器
- (void)startServer
{
    NSError *error;
    [_serverSocket acceptOnPort:54321 error:&error];
    if (error) {
        NSLog(@"服务器开启失败");
    }else {
        NSLog(@"服务器开启成功");
    }
}

#pragma mark 有客户端建立连接
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
    //这里sock指服务器的socket,newSocket指客户端的Socket,
    NSLog(@"%@, %@", sock, newSocket);

    //保存客户端的Socket,不然刚连接成功就会自动关闭
    [self.clientSocket addObject:newSocket];

    //sock只负责连接服务器,不负责读取数据,因此使用newSocket
    [newSocket readDataWithTimeout:-1 tag:100];
}

#pragma mark 接收客户端发送过来的数据
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
    //这里的sock指客户端的Socket
    NSLog(@"%@", sock);
    NSString *receiveStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

    //去除换行符、回车符
    receiveStr = [receiveStr stringByReplacingOccurrencesOfString:@"\r" withString:@""];
    receiveStr = [receiveStr stringByReplacingOccurrencesOfString:@"\n" withString:@""];

    if ([receiveStr hasPrefix:@"iam"]) {

        NSString *user = [receiveStr componentsSeparatedByString:@":"][1];

        NSString *responseStr = [user stringByAppendingString:@" has joined\n"];

        [sock writeData:[responseStr dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
    }

    if ([receiveStr hasPrefix:@"msg"]) {

        NSString *msg = [[receiveStr componentsSeparatedByString:@":"][1] stringByAppendingString:@"\n"];

        [sock writeData:[msg dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
    }

    if ([receiveStr isEqualToString:@"quit"]) {

        [sock disconnect];

        [self.clientSocket removeObject:sock];
    }
}

#pragma mark 服务器发送数据给客户端
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
    //这里的sock依然指客户端的socket
    NSLog(@"%@", sock);

    //服务器每次写数据之前都需要读取一次数据,之后才可以返回数据
    [sock readDataWithTimeout:-1 tag:100];
}

@end

4.2 开启服务器

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        ChatServer *chatServer = [[ChatServer alloc] init];
        [chatServer startServer];

        //主线程永远不会销毁,在这里开启运行循环可以保证聊天室的服务器不会自动关闭
        [[NSRunLoop currentRunLoop] run];

    }
    return 0;
}

4.3 测试

可以使用上面的iOS程序进行测试,也可以在命令行中测试,如图:
技术分享

Socket编程(简易聊天室客户端/服务器编写、CocoaAsyncSocket)

标签:

原文地址:http://blog.csdn.net/u011330638/article/details/51901294

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!