Socket编程简介之iOS笔记摘录

目录
    1. Socket 介绍
    2. Socket使用(客户端)
    3. socket原生方法详解

1. Socket 介绍

一种网络通信方式,对TCP/IP协议进行封装了的API(基于C语言编写的),本质并不是协议。

Socket所处位置
HTTP基于短连接,每次请求数据都会重新建立连接。
  基于TCP
Socket基于长连接,只有主动断开连接才会断开连接(实际中,因为防火墙会主动断开长时间不活跃的连接,所以通常会使用轮询保持长连接)。
  分2种:
    1、面向连接(基于TCP),必须指定一个socket目的地。
    2、非连接(基于UDP),无需指定一个socket目的地。

  1、纯C语言的,跨平台
  2、网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:
    1、连接使用的协议    (2端传输使用的协议,TCP/UDP)
    2、本地主机的IP地址  (标识源主机)
    3、本地进程的协议端口 (标识源应用)
    4、远地主机的IP地址  (标识目标主机)
    5、远地进程的协议端口 (标识目标应用)

优点
  1、传输数据为字节级,可自定义,数据量小,时间短,性能高。
  2、适合实时交互
  3、可加密(数据安全性高)

建立Socket连接至少需要一对套接字
  1、一个运行于客户端,称为ClientSocket,
  2、一个运行于服务器端,称为ServerSocket。

连接过程

  1、服务器监听:
  服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
  2、客户端请求:
  客户端的套接字提出连接请求,要连接的目标是服务器端的套接字(必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求)。
  3、连接确认:
  当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。


======具体如下======

TCP  (3次握手连接、4次握手断开)
  服务器端的步骤是:
  1、用socket()函数创建一个socket;用函数setsockopt()设置socket属性
  2、用bind()函数绑定IP地址、端口等信息到socket上;
  3、用listen()函数开启监听;
  5、用accept()函数接收客户端上来的连接
  6、用send()和recv() 或 read()和write() 函数收发数据;
  7、关闭网络连接、关闭监听;
  客户端的步骤为:
  1、用socket()函数创建一个socket;setsockopt()函数设置socket属性
  2、用bind()函数绑定IP地址、端口等信息到socket上  
  3、用connect()函数连接服务器
  4、用send()和recv(),或者read()和write()函数收发数据;  
  5、关闭网络连接

UDP
  服务器端的步骤是:
  1、用socket()函数创建一个socket;用setsockopt()函数设置socket属性
  2、用bind()函数绑定IP地址、端口等信息到socket上
  3、用recvfrom()函数循环接收数据
  4、关闭网络连接;
  客户端一般步骤是:
  1、用socket()函数创建一个socket;用setsockopt()函数设置socket属性
  2、用bind()函数绑定IP地址、端口等信息到socket
  3、发送数据,用函数sendto()
  4、关闭网络连接;
Socket的TCP连接

2. Socket使用(客户端)

绝大多数代码都是固定的

方式一:原生Socket使用(BSD socket)

#import <arpa/inet.h>
#import <netinet/in.h>
#import <sys/socket.h>
{
int _clientSocket;
}


- (void)viewDidLoad {
    [super viewDidLoad];

    [self connectToServer:@"192.168.100.25" port:1212];
    NSString *content=[self sentAndRecv:@"Hello World !"];
    NSLog(@"");
/*
例:发送HTTP请求,加载Baidu。

    [self connectToServer:@"115.239.210.27" port:80];
    //
    NSString *requestStr =@"GET / HTTP/1.1\r\n"
    "Host: www.baidu.com\r\n"
    "Connection: close\r\n\r\n";
    NSString *content=[self sentAndRecv:requestStr];
    NSString *contentStr=[[content componentsSeparatedByString:@"\r\n\r\n"]lastObject];
    //
    UIWebView *webV=[UIWebView new];
    [webV loadHTMLString:contentStr baseURL:[NSURL URLWithString:@"https://www.baidu.com/"]];
    [self.view addSubview:webV];
    [webV setFrame:self.view.bounds];
*/
}
-(void)delloc{
    close(_clientSocket);
}
/**
 连接服务器

 @param ip 服务器IP
 @param port 服务器端口
 */
- (void)connectToServer:(NSString *)ip port:(int)port {
    /*
     1、创建socket
     
     1.AF_INET: ipv4 执行ip协议的版本
     2.SOCK_STREAM:指定Socket类型,面向连接的流式socket 传输层的协议
     3.IPPROTO_TCP:指定协议。 IPPROTO_TCP 传输方式TCP传输协议
     返回值 大于0 创建成功
     */
    _clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    /*
     2、连接服务器
     
     参数一:套接字描述符
     参数二:指向数据结构sockaddr的指针,其中包括目的端口和IP地址
     参数三:参数二sockaddr的长度,可以通过sizeof(struct sockaddr)获得
     返回值 int -1失败 0 成功
     */
    struct sockaddr_in addr;
    /* 填写sockaddr_in结构*/
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip.UTF8String);
    int connectResult = connect(_clientSocket, (const struct sockaddr *)&addr, sizeof(addr));
    if (connectResult == 0) {
        NSLog(@"conn ok");
    }
}
/**
 向服务器发送数据

 @param msg 数据
 @return 从服务器获取返回的数据
 */
- (NSString *)sentAndRecv:(NSString *)msg {
    /*
     3、发送数据
     
     第一个参数指定发送端套接字描述符;
     第二个参数指明一个存放应用程式要发送数据的缓冲区;
     第三个参数指明实际要发送的数据的字符数;
     第四个参数一般置0。
     成功则返回实际传送出去的字符数,失败返回-1,
     */
    const char *str = msg.UTF8String;
    // 发消息
    ssize_t sendLen = send(_clientSocket, str, strlen(str), 0);
    /*
     4、接收数据
     
     第一个参数socket
     第二个参数存放数据的缓冲区
     第三个参数缓冲区长度。
     第四个参数指定调用方式,一般置0
     返回值 接收成功的字符数
     */
    // 收消息
    char *buf[1024];
    ssize_t recvLen = recv(_clientSocket, buf, sizeof(buf), 0);
    // 
    NSMutableString *muString=[NSMutableString new];
    while (recvLen > 0) {
        
        NSString *recvStr = [[NSString alloc] initWithBytes:buf length:recvLen encoding:NSUTF8StringEncoding];
        [muString appendString:recvStr];
        recvLen = recv(_clientSocket, buf, sizeof(buf), 0);
    }
    return [muString copy];
}

方式二:原生Socket使用2(CFNetwork)

<NSStreamDelegate>{
    // 输入流,用来读取服务器返回的字节
    NSInputStream *inputStream;
    // 输出流,用于给服务器发送字节
    NSOutputStream *outputStream;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self connectToServer:@"192.168.100.25" port:1212];
}
// 建立与服务器的连接
-(void)connectToServer:(NSString *)host port:(int)port{
    // 创建CF下的读入流、写出流
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;
    
    // 创建流
    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)(host), port, &readStream, &writeStream);
    // 建立对应关系(CF流和NS流)
    inputStream = (__bridge NSInputStream *)(readStream);
    outputStream = (__bridge NSOutputStream *)(writeStream);

    // 设置代理
    inputStream.delegate = self;
    outputStream.delegate = self;
    
    // 将流对象添加到主运行循环(如果不加到主循环,Socket流是不会工作的)
    [inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    [outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    
    // 打开流
    [inputStream open];
    [outputStream open];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //
    NSData *data = [[NSData alloc] initWithData:[@"Hello World" dataUsingEncoding:NSUTF8StringEncoding]];
    [outputStream write:[data bytes] maxLength:[data length]];
}

#pragma mark NSStreamDelegate
-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
    switch (eventCode) {
        case NSStreamEventOpenCompleted:
            NSLog(@"连接服务器后进入");
            break;
        case NSStreamEventHasBytesAvailable:
        {
            NSLog(@"从服务器获取数据后进入");
            uint8_t buffer[10];
            NSMutableString *mstr = [NSMutableString string];
            NSInteger len;
            do{
                len =  [inputStream read:buffer maxLength:sizeof(buffer)];
                NSString *s = [[NSString alloc] initWithBytes:buffer length:len encoding:NSUTF8StringEncoding];
                [mstr appendString:s];
            }while (len == sizeof(buffer));
            
            NSLog(@"从服务器获取的数据:%@",mstr);
        }
            break;
        case NSStreamEventHasSpaceAvailable:
        {
            NSLog(@"允许写入数据时进入");
        }
            break;
        case NSStreamEventErrorOccurred:{
            NSLog(@"发生错误时进入");
        }
            break;
        case NSStreamEventEndEncountered:
            NSLog(@"流结束时进入");
            // 做善后工作,关闭流的同时,将流从主运行循环中删除
            [aStream close];
            [aStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        default:
            break;
    }
}

方式三:三方CocoaAsyncSocket使用

Podfile中+
  pod 'CocoaAsyncSocket'
#import <CocoaAsyncSocket/GCDAsyncSocket.h>
<GCDAsyncSocketDelegate>
@property (nonatomic,strong) GCDAsyncSocket *socket;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // IP、端口、error
    NSString *host = @"192.168.100.25";
    int port = 1212;
    NSError *error = nil;
    
    // 创建一个socket对象,并连接
    _socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
    [_socket connectToHost:host onPort:port error:&error];
    if (error) {
        NSLog(@"%@",error.userInfo);
    }
    
    // 发送数据
    [_socket writeData:[@"Hello World!" dataUsingEncoding:NSUTF8StringEncoding] withTimeout:1.0 tag:123];
}


#pragma mark - Socket代理方法
// 连接服务器成功时调用
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    NSLog(@"%s",__func__);
    NSLog(@"连接成功");
}
// 断开和服务器的连接时调用
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    if (err) {
        NSLog(@"连接失败");
    } else {
        NSLog(@"正常断开");
    }
}
// 客户端发送数据后调用(即调用writeData后)
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
    
    NSLog(@"%s",__func__);
    
    // 监听接收服务端传过来的数据。-1不设置超时。
    [sock readDataWithTimeout:-1 tag:tag];
}

// 读取到服务器端的数据后调用
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    NSString *receiverStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%s %@",__func__,receiverStr);
}
-(void)dealloc{
    
    _socket.delegate = nil;
    [_socket disconnect];
    _socket = nil;
}

使用YYNetWork作为Socket服务器

使用YYNetWork作为Socket服务器

3. socket原生方法详解

1、int socket(int domain, int type, int protocol);

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符,它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
socket函数的三个参数分别为:
    domain:即协议域(协议族)。常用的协议族有:AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
    type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
    protocol:故名思意,就是指定协议。常用的协议有:IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

注意:
  1、type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
  2、当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

2、int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

三个参数:
    addrlen:地址的长度
    sockfd:即socket描述字,通过socket()函数创建获取(唯一标识一个socket)。
    addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同。
      ipv4对应的是:
        struct sockaddr_in {
            sa_family_t    sin_family; /* address family: AF_INET */
            in_port_t      sin_port;   /* port in network byte order */
            struct in_addr sin_addr;   /* internet address */
        };
        /* Internet address. */
        struct in_addr {
            uint32_t       s_addr;     /* address in network byte order */
        };
      ipv6对应的是:
        struct sockaddr_in6 {
            sa_family_t     sin6_family;   /* AF_INET6 */
            in_port_t       sin6_port;     /* port number */
            uint32_t        sin6_flowinfo; /* IPv6 flow information */
            struct in6_addr sin6_addr;     /* IPv6 address */
            uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */
        };
        struct in6_addr {
            unsigned char   s6_addr[16];   /* IPv6 address */
        };
      Unix域对应的是:
        #define UNIX_PATH_MAX    108
        struct sockaddr_un {
            sa_family_t sun_family;               /* AF_UNIX */
            char        sun_path[UNIX_PATH_MAX];  /* pathname */
        };

在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序

3、int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户端通过调用connect函数来建立与TCP服务器的连接。

三个参数:
  sockfd:客户端的socket描述字
  addr:服务器的socket地址
  socklen_t:socket地址的长度。

4、int listen(int sockfd, int backlog);

服务器在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

三个参数:
  sockfd:客户端的socket描述字
  backlog:最大连接个数

5、read()、write()等函数

网络I/O操作有下面几组:
    read()/write()
    recv()/send()
    readv()/writev()
    recvmsg()/sendmsg() 推荐
    recvfrom()/sendto()

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

#include <sys/types.h>
#include <sys/socket.h>
//
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
//
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

read函数是负责从fd中读取内容。
成功时,返回值大于0,返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了
失败时,返回值小于0。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

write函数将buf中的nbytes字节内容写入文件描述符fd.
成功时,返回值大于0,表示写了部分或者是全部的数据。
失败时,返回值小于0,此时出现了错误。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

  1. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。

如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

3个参数:
  sockfd:服务器的socket描述字,
  addr:指向struct sockaddr *的指针,用于返回客户端的协议地址,
  addrlen:协议地址的长度。
  1. int close(int fd);
#include <unistd.h>
close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,924评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,781评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,813评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,264评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,273评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,383评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,800评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,482评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,673评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,497评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,545评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,240评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,802评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,866评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,101评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,673评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,245评论 2 341

推荐阅读更多精彩内容