目录
1. Socket 介绍
2. Socket使用(客户端)
3. socket原生方法详解
1. Socket 介绍
一种网络通信方式,对TCP/IP协议进行封装了的API(基于C语言编写的),本质并不是协议。
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、关闭网络连接;
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服务器
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表示网络连接出现了问题(对方已经关闭了连接)。
- 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:协议地址的长度。
- int close(int fd);
#include <unistd.h>
close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。