Socket理解和使用

1、TCP/IP

TCP/IP就是传输控制协议/网间协议,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)
下面说一下IP地址和端口号

IP地址

IP地址用于唯一标识网络中的一个通信实体,这个通信实体既可以是一台主机,也可以是一台打印机,或者是路由器的某一个端口。在基于IP协议的网络中传输的数据包,都必须使用IP地址在进行标识。IP地址用于唯一标识网络上的一个通信实体,但一个通信实体可以有多个通信程序同时提供网络服务,此时还需要使用端口。

端口

端口是一个16位的整数,用于表示数据交给哪个通信程序处理。一次端口就是应用程序与外界交流的出入口,它是一种抽象的软件结构,包括一些数据结构I/O(基本输入输出)缓冲区。不同的应用程序处理不同端口上的数据,同一台机器上不能有两个程序使用同一个端口,端口号从0到65535。
TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中,
应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
传输层:TCP,UDP
网络层:IP,ICMP,OSPF,EIGRP,IGMP
数据链路层:SLIP,CSLIP,PPP,MTU

2、Socket

利用ip地址+协议+端口号唯一标示网络中的一个进程,就可以利用socket进行通信了,我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。


socket通信流程

socket是"打开—读/写—关闭"模式的实现,以使用TCP协议通讯的socket为例,其交互流程大概是这样子的

socket编程API

这里简单解释一下方法作用和参数

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

根据指定的地址族、数据类型和协议来分配一个socket的描述字及其所用的资源。
domain:协议族,常用的有AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE其中AF_INET代表使用ipv4地址。
type:socket类型,常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等
protocol:协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等

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

把一个地址族中的特定地址赋给socket
sockfd:socket描述字,也就是socket引用
addr:要绑定给sockfd的协议地址
addrlen:地址的长度

int listen(int sockfd, int backlog);

监听socket
sockfd:要监听的socket描述字
backlog:相应socket可以排队的最大连接个数

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

连接某个socket
sockfd:客户端的socket描述字
addr:服务器的socket地址
addrlen:socket地址的长度

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

TCP服务器监听到客户端请求之后,调用accept()函数取接收请求
sockfd:服务器的socket描述字
addr:客户端的socket地址
addrlen:socket地址的长度

ssize_t read(int fd, void *buf, size_t count);

读取socket内容
fd:socket描述字
buf:缓冲区
count:缓冲区长度

ssize_t write(int fd, const void *buf, size_t count);

向socket写入内容,其实就是发送内容
fd:socket描述字
buf:缓冲区
count:缓冲区长度

int close(int fd);

socket标记为以关闭 ,使相应socket描述字的引用计数-1,当引用计数为0的时候,触发TCP客户端向服务器发送终止连接请求。

3、使用Socket实现TCP服务器端和客户端

3.1 服务器端如下

#import "ViewController.h"
#import <arpa/inet.h>
#import <sys/socket.h>
#import <ifaddrs.h>

// 最大连接客户端数量
static int const kMaxConnectCount = 10;

@interface ViewController ()
// 服务器端socket
@property (nonatomic, assign) int serverSocket;
// 接收消息时客户端socket
@property (nonatomic, assign) int newSocket;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

// 点击start按钮
- (IBAction)startConnect:(UIButton *)sender {
    // 1.创建socket
    /*
     * socket返回一个int值,-1为创建失败
     * 第一个参数指明了协议族/域 ,通常有AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL
     * 第二个参数指定一个套接口类型:SOCK_STREAM,SOCK_DGRAM、SOCK_SEQPACKET等
     * 第三个参数指定相应的传输协议,常用的:IPPROTO_TCP、IPPTOTO_UDP,一般设置为0来使用这个默认的值,会根据第二个参数自动选择合适的,一般SOCK_STREAM对应TCP协议,SOCK_DGRAM对应UDP协议
     */
    int server_Socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_Socket == -1) {
        NSLog(@"创建失败");
        // 创建失败,要关闭socket
        close(server_Socket);
        return;
    }
    self.serverSocket = server_Socket;
    
    // 2.绑定地址和端口
    // 地址结构体数据,记录ip和端口号
    struct sockaddr_in server_addr;
    // 声明使用的协议
    server_addr.sin_family = AF_INET;
    
    // 设置端口号,htons()是将整型变量从主机字节顺序转变成网络字节顺序
    server_addr.sin_port = htons(12345);
    
    // 获取本机的ip,转换成char类型的
    NSString *ipStr = [self getIPAddress];
    const char *ip = [ipStr cStringUsingEncoding:NSASCIIStringEncoding];
    // ip可以用("127.0.0.1")本机地址
    server_addr.sin_addr.s_addr = inet_addr(ip);
    
    /*
     * bind函数用于将套接字关联一个地址,返回一个int值,-1为失败
     * 第一个参数指定套接字,就是前面socket函数调用返回额套接字
     * 第二个参数为指定的地址
     * 第三个参数为地址数据的大小
     */
    int bind_result = bind(server_Socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (bind_result == -1) {
         NSLog(@"绑定端口失败");
        close(self.serverSocket);
        return;
    }
    
    // 3.监听绑定的地址
    /*
     * listen函数使用主动连接套接接口变为被连接接口,使得可以接受其他进程的请求,返回一个int值,-1为失败
     * 第一个参数是之前socket函数返回的套接字
     * 第二个参数可以理解为连接的最大限制
     */
    int ls = listen(self.serverSocket, kMaxConnectCount);
    if (ls == -1) {
        NSLog(@"监听失败");
        close(self.serverSocket);
        return;
    }
    
    // 4,开启一个子线程用于数据的接收
    NSThread *recvThread = [[NSThread alloc] initWithTarget:self selector:@selector(recvData) object:nil];
    [recvThread start];
}

// 等待客户端的连接,使用accept()(由于accept函数会阻塞线程,在等待连接的过程中会一直卡着,所以建议将其放在子线程里面)
- (void)recvData {
    /*
     * accept()函数在连接成功后会返回一个新的套接字(self.newSocket),用于之后和这个客户端之前收发数据
     * 第一个参数为之接前监听的套字,之前是局部变量,现在需要改为全局的
     * 第二个参数是一个结果参数,它用来接收一个返回值,这个返回值指定客户端的地址
     * 第三个参数也是一个结果参数,它用来接收recvAddr结构体的代销,指明其所占的字节数
     */
    struct sockaddr_in client_address;
    socklen_t address_len = 0;
    int client_socket = accept(self.serverSocket, (struct sockaddr *)&client_address, &address_len);
    self.newSocket = client_socket;
    if (client_socket == -1) {
         NSLog(@"接受客户端链接失败");
        return;
    }
    
    while (1) {
        // 接收客户端传来的数据
        char buf[1024] = {0};
        long iReturn = recv(client_socket, buf, 1024, 0);
        if (iReturn > 0) {
            NSLog(@"客户端来消息了");
            NSString *str = [NSString stringWithCString:buf encoding:NSUTF8StringEncoding];
            NSLog(@"%@",str);
        }else if (iReturn == -1) {
            NSLog(@"读取消息失败");
            break;
        }else if (iReturn == 0) {
            NSLog(@"客户端走了");
            close(client_socket);
            break;
        }
    }
    
}

- (IBAction)sendMessage:(UIButton *)sender {
    NSString *msg = @"hello client";
    char *buf[1024] = {0};
    const char *p1 = (char*)buf;
    p1 = [msg cStringUsingEncoding:NSUTF8StringEncoding];
    send(self.newSocket, p1, 1024, 0);
}


// Get IP Address
- (NSString *)getIPAddress {
    NSString *address = @"error";
    struct ifaddrs *interfaces = NULL;
    struct ifaddrs *temp_addr = NULL;
    int success = 0;
    // retrieve the current interfaces - returns 0 on success
    success = getifaddrs(&interfaces);
    if (success == 0) {
        // Loop through linked list of interfaces
        temp_addr = interfaces;
        while(temp_addr != NULL) {
            if(temp_addr->ifa_addr->sa_family == AF_INET) {
                // Check if interface is en0 which is the wifi connection on the iPhone
                if([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
                    // Get NSString from C String
                    address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
                }
            }
            temp_addr = temp_addr->ifa_next;
        }
    }
    // Free memory
    freeifaddrs(interfaces);
    NSLog(@"%@",address);
    return address;
}

3.2 客户端实现

#import "ViewController.h"
#import <arpa/inet.h>
#import <sys/socket.h>
#import <ifaddrs.h>

@interface ViewController ()
@property (nonatomic, assign) int sock;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (IBAction)connectBtnClick {
    // 1.创建socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == -1) {
        NSLog(@"socket error : %d",sock);
        return;
    }
     // getIPAddress该方法和服务器端一样
    NSString *host = [self getIPAddress];
    const char *ip = [host cStringUsingEncoding:NSASCIIStringEncoding];
    //2,获取主机的地址,绑定地址和端口
    struct sockaddr_in server_addr;
    server_addr.sin_len = sizeof(struct sockaddr_in);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(12345);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    bzero(&(server_addr.sin_zero), 8);
    
    //接受客户端的链接
    /*
     * connect函数通常用于客户端建立tcp连接,连接指定地址的主机,函数返回一个int值,-1为失败
     * 第一个参数为socket函数创建的套接字,代表这个套接字要连接指定主机
     * 第二个参数为套接字sock想要连接的主机地址和端口号
     * 第三个参数为主机地址大小
     */
    int con = connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (con == -1) {
        close(sock);
        NSLog(@"连接失败");
        return;
    }
    NSLog(@"连接成功");
    self.sock = sock;
    
    // 开启一个子线程用于接收数据
    NSThread *recvThread = [[NSThread alloc] initWithTarget:self selector:@selector(recvData) object:nil];
    [recvThread start];
}

- (void)recvData{
    while (1) {
        //接受服务器传来的数据
        char buf[1024];
        long iReturn = recv(self.sock, buf, 1024, 0);
        if (iReturn > 0) {
            NSString *str = [NSString stringWithCString:buf encoding:NSUTF8StringEncoding];
            NSLog(@"服务器端来消息了");
            NSLog(@"接收到的数据:%@",str);
        }else if (iReturn == -1) {
            NSLog(@"接受失败-1");
            break;
        }
    }
}

- (IBAction)sendMsg {    
    NSString *msg = @"hello service";
    char *buf[1024] = {0};
    const char *p1 = (char*)buf;
    p1 = [msg cStringUsingEncoding:NSUTF8StringEncoding];
    send(self.sock, p1, 1024, 0);
}

4、使用CFSocket实现TCP客户端

创建Socket

// 创建Socket,无需回调函数函数
    _socket = CFSocketCreate(kCFAllocatorDefault
        , PF_INET // 指定协议族,如果该参数为0或者负数,则默认为PF_INET
        , SOCK_STREAM // 如果协议族为PF_INET,默认为SOCK_STREAM
        , IPPROTO_TCP // 指定通信协议。如果前一个参数为SOCK_STREAM,默认使用TCP协议
        , kCFSocketNoCallBack // 该参数指定下一个回调函数所监听的事件类型
        , nil
        , NULL);
    if (_socket != nil)
    {
        // 定义sockaddr_in类型的变量,该变量将作为CFSocket的地址
        struct sockaddr_in addr4;
        memset(&addr4, 0, sizeof(addr4));
        addr4.sin_len = sizeof(addr4);
        addr4.sin_family = AF_INET;
        // 设置连接远程服务器的地址,此处设置的位本机地址
        addr4.sin_addr.s_addr = inet_addr("10.9.39.111");
        // 设置连接远程服务器的监听端口
        addr4.sin_port = htons(30000);
        // 将IPv4的地址转换为CFDataRef
        CFDataRef address = CFDataCreate(kCFAllocatorDefault
            , (UInt8 *)&addr4, sizeof(addr4));
        // 连接远程服务器器的Socket,并返回连接的结果
        CFSocketError result = CFSocketConnectToAddress(_socket
            , address // 指定远程服务器的IP和端口
            , 5  // 指定连接超时时长,如果该参数为负数,则把连接操作放在后台进行,
            // 当_socket消息类型为kCFSocketConnectCallBack,
            // 将会在连接成功或失败的时候在后台触发回调函数
            );
        // 如果连接远程服务器成功
        if(result == kCFSocketSuccess)
        {
            isOnline = YES;
            // 启动新线程来读取服务器响应的数据
            [NSThread detachNewThreadSelector:@selector(readStream)
                                     toTarget:self withObject:nil];
        }
    }

读取接收的数据

- (void)readStream
{
    char buffer[2048];
    long hasRead;
    //与本机关联的Socket 如果已经失效返回-1:INVALID_SOCKET
    while ((hasRead = recv(CFSocketGetNative(_socket)
        , buffer, sizeof(buffer), 0)))
    {
        NSString* content = [[NSString alloc] initWithBytes:buffer
            length:hasRead encoding:NSUTF8StringEncoding];
        // 使用主线程来更新UI控件的状态
        dispatch_async(dispatch_get_main_queue(), ^{
            self.showView.text = [NSString stringWithFormat:@"%@\n%@",
                content ,self.showView.text];
        });
    }
}

//发送数据
const char* data = [messageTosend UTF8String];
send(CFSocketGetNative(_socket), data, strlen(data) + 1 , 1);


ios 服务器端文件编译:
先进入目标文件夹下
执行终端命令如下:
$ clang -fobjc-arc -framework Foundation SimpleServer.m
则会生成一个a.out 文件,然后运行a.out即可
a.out 文件的运行方式
$ chmod +x a.out
$ ./a.out


出现上面所示则代表服务器绑定成功


然后运行客户端输入聊天信息即可收到,如图所示

详细代码和服务器端代码(SimpleServer.m)下载 详细demo下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容