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

Socket理解和使用_第1张图片

2、Socket

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


Socket理解和使用_第2张图片

socket通信流程

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

Socket理解和使用_第3张图片

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 
#import 
#import 

// 最大连接客户端数量
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 
#import 
#import 

@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

Socket理解和使用_第4张图片

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

Socket理解和使用_第5张图片

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

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

你可能感兴趣的:(Socket理解和使用)