Socket网络编程
TCP/IP
要想理解socket首先得熟悉一下TCP/IP协议族, TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,定义了主机如何连入因特网及数据如何在它们之间传输的标准.
从字面意思来看TCP/IP是TCP和IP协议的合称,但实际上TCP/IP协议是指因特网整个TCP/IP协议族。不同于OSI模型的七个分层,TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中.
应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
传输层:TCP,UDP
网络层:IP,ICMP,OSPF,EIGRP,IGMP
数据链路层:SLIP,CSLIP,PPP,MTU
每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这样子的.
Snip20160501_1.pngsocket
我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号
唯一标示网络中的一个进程。
1、IP地址[主机名]
- 英文全称:Internet Protocol Address,又译为网际协议地址。
- 网络中设备的标识,用来唯一标识每一台计算机。现在常用的IP地址是IPV4地址。
- IPV4就是有4段数字,格式是xxx.xxx.xxx.xxx。每一段数字由8位二进制做成,取值范围是0~255。
- IPV4采用32位地址长度,只有大约43亿个地址。IPv4定义的有限地址空间将被耗尽。
- 为了扩大地址空间,拟通过IPV6重新定义地址空间。IPv6采用128位地址长度。几乎可以不受限制地提供地址。但IPV6现在还没有正式普及。
- 为了解决IPV4有限地址空间的问题,IP地址又分内网地址和外网地址。(比如校园网,每一个学生都会有一个内网地址,学校会有一个路由器,路由器会有个外网地址,学生想要上外网都必须通过路由器出去,只要通过同一个路由器出去的,他们对应的外网地址都是一样的)
- 本质上所有的网络访问是通过ip地址访问的。域名是一个速记符号,不用记住IP地址复杂的数字。
- 本地回环地址:127.0.0.1主机名:localhost
- 每台计算机都有一个127.0.0.1
- 如果127.0.0.1 ping不通,说明网卡不工作(比如装黑苹果,检测网卡驱动有没装好,可以ping下回环地址)
- 如果本机地址ping不通,说明网线坏了。
TIP:通过ip138.com可以速查某个域名对应的IP地址。
2、端口号
- 通过打电话例子说明端口号的作用
- 很多网络概念来源于电话
- 电话号码类似IP
- 分机号类似于端口
- 端口号的作用
- 用来标识进程的逻辑地址,不同进程的标识。
- 有效的端口:0~65535。
- 其中0~1024由系统使用或保留端口。开发中不要使用1024以下的端口。
- 端口有什么用?我们知道,一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP地址与网络服务的关系是一对多的关系。实际上是通过“IP地址+端口号”来区分不同的服务的。
3、传输协议(通信规则)
-
UDP(用户数据报协议)
○ 面向无连接的协议。
○ 只管发送,不确认对方是否接收到。
○ 将数据及源和目的封装成数据包中,不需要建立连接。
○ 每个数据包的大小限制在64K之内。
○ 因为无需连接,因此是不可靠协议。
○ 不需要建立连接,速度快。
○ 理解发电报的特点就理解了UDP协议的特点。 -
TCP(Transmission Control Protocol,传输控制协议)
○ 面向连接的协议。
○ 建立连接,形成传输数据的通道。
○ 连接中进行大数据传输(数据大小不受限制)。
○ 通过三次握手完成连接,是可靠协议,安全送达。
○ 必须建立连接,效率会稍低。
○ 简单的描述下三次握手的过程:
主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”,这是第一次对话;主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话;主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”,这是第三次对话。三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据。
注意:断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”
TCP和UDP使用场合
tcp一般用于:
文件传输(ftp http对数据准确性要求高,速度可以相对慢)。
发送或接收邮件(pop imap smtp 对数据准确性要求高,非紧急应用)。
远程登录(telnet ss 对数据准确性有一定要求,有连接的概念)。
UDP一般用于
即时通信(qq聊天对数据准确性和丢包要求比较低,但速度必须快)。
在线视频(rtsp速度一定要快,保证视频连续,但是偶尔花了一个图像帧,人们还是能接受的)。
网络语音电话(VoIP语音数据包一般比较小,需要高速发送,偶尔断音或串音也没有问题)。
网络通讯三要素归纳一句话:通过 ip找机器,通过 端口找程序,通过 协议 确定如何传输数据。
能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。
Snip20160501_2.pngsocket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
socket通信流程
socket是"打开—读/写—关闭"模式的实现,以使用TCP协议通讯的socket为例,其交互流程大概是这样子的
Snip20160501_3.png文字描述:
服务器:
服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket
服务器为socket绑定ip地址和端口号
服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开
客户端:
客户端创建socket
客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket
服务器:
服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞
状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求
客户端:
客户端连接成功,向服务器发送连接状态信息
服务器:
服务器accept方法返回,连接成功
客户端:
客户端向socket写入信息
服务器:
服务器读取信息
客户端关闭
服务器端关闭
三次握手
在TCP/IP协议中,TCP协议通过三次握手建立一个可靠的连接
第一次握手:客户端尝试连接服务器,向服务器发送syn包(同步序列编号Synchronize Sequence Numbers),syn=j,客户端进入SYN_SEND状态等待服务器确认
第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手
定睛一看,服务器socket与客户端socket建立连接的部分其实就是大名鼎鼎的三次握手
socket编程API
前面提到socket是"打开—读/写—关闭"模式的实现,简单了解一下socket提供了哪些API供应用程序使用,还是以TCP协议为例,看看Unix下的socket API,Socket 是纯 C语言的,是跨平台的。
访问本地服务器
1、导入三个头文件
#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>
2、创建socket
/**
参数
domain: 协议域/协议族,AF_INET(IPV4的网络开发)
type: Socket 类型,SOCK_STREAM(TCP)/SOCK_DGRAM(UDP,报文)
protocol: IPPROTO_TCP,协议,如果输入0,可以根据第二个参数自动选择协议
返回值
socket,如果>0 就表示成功
*/
int clientSocket = socket(AF_INET, SOCK_STREAM,0);
3、连接到服务器
/**
参数
1> 客户端socket
2> 指向数据结构sockaddr的指针,其中包括目的端口和IP地址。即服务器的“结构体”地址
3> 结构体数据长度
返回值
0 成功/其他 错误代号,非0即真
*/
struct sockaddr_in serverAddress;
//协议族
serverAddress.sin_family = AF_INET;
//ip 找机器
serverAddress.sin_addr.s_addr = inet_addr("127.0.0.1");
//端口号找程序
serverAddress.sin_port = htons(12345);
// C语言函数如果需要传递结构体的地址,一般都需要将结构体长度传递过去
//连接服务器
int result = connect(clientSocket, (const struct
sockaddr *)&serverAddress, sizeof(serverAddress));
if (result == 0)
{
NSLog(@"成功");
}else {
NSLog(@"失败");
}
友情提示
在C语言开发时,如果要传递结构体的地址,通常会一起传递结构的长度。因为C 语言中取数据是通过指针
寻址的,告诉长度的目的是防止取错数据。
在终端输入:nc -lk 12345 相当于在本机上启动了一个服务器,ip是本机地址,端口号是12345。
// 发送数据
/**
参数
1> 客户端socket
2> 发送内容地址 void * == id
3> 发送内容长度,是指字节的长度。
4> 发送方式标志,一般为0
返回值
如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR
*/
NSString *msg = @"约吗?";
ssize_t sendLength = send(clientSocket, msg.UTF8String, strlen(msg.UTF8String), 0);
NSLog(@"发送了%ld长度的字节,%zd,%zd",sendLength,msg.length,strlen(msg.UTF8String));
// 读取数据
/**
参数
1> 客户端socket
2> 接受内容地址 void * == id
3> 接收内容长度,是指字节的长度。告诉服务器一次最多只能接收多少字节的内容。
4> 接收标志,一般填0,表示阻塞式的,一直等待服务器返回数据
返回值
如果成功,则返回接收的字节数,失败则返回SOCKET_ERROR
*/
// 缓冲区,准备接受来自服务器的数据
//C语言中,数组的名字,就是指向数组第一个元素的指针。
unsigned char buffer[1024];
ssize_t recvLength = recv(clientSocket, buffer, sizeof(buffer), 0);
// 将buffer转换成二进制数据
NSData *data = [NSData dataWithBytes:buffer length:recvLength];
// 将二进制转换字符串
NSString *resultStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"接收了%ld长度的字节,内容是:%@",recvLength,resultStr);
// 断开链接
close(clientSocket);
控制台打印的信息:
Snip20160501_6.png终端收到发送的内容:
Snip20160501_5.pngSocket发送HTTP请求远程服务器(百度,京东,起点)
#import "ViewController.h"
#import <sys/socket.h>
#import <arpa/inet.h>
#import <netinet/in.h>
@interface ViewController ()
// 客户端socket
@property (nonatomic, assign) int clientSocket;
@property (nonatomic, weak) IBOutlet UIWebView *webView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self connectiontToRemoteServer];
}
- (void)connectiontToRemoteServer{
NSLog(@"come on");
// 访问百度网页
//因为浏览网页服务默认的端口号都是80
if (![self connectionWithHost:@"106.39.164.186" port:80])return;
NSLog(@"连接成功");
// 发送请求给服务器
NSString *request = @"GET / HTTP/1.1\r\n"
"Connection:Close\r\n"
"Host:m.jd.com\r\n\r\n";
// URL:协议头://主机地址/资源路径
NSString *result = [self sendAndRecv:request];
NSLog(@"result = %@",result);
// 查找 \r\n\r\n
NSRange range = [result rangeOfString:@"\r\n\r\n"]; // 在字符串中查找子串
NSLog(@"%zd--%zd",range.location,range.length);
if (range.location != NSNotFound) { // 查找到了 \r\n\r\n
result = [result substringFromIndex:range.location ];
}
[self.webView loadHTMLString:result baseURL:[NSURL URLWithString:@"http://m.jd.com"]];
}
/**
* 建立Socket连接
*/
- (BOOL)connectionWithHost:(NSString *)host port:(int)port{
// 创建客户端Socket
/**
参数
domain: 协议域/协议族,AF_INET(IPV4的网络开发)
type: Socket 类型,SOCK_STREAM(TCP)/SOCK_DGRAM(UDP,报文)
protocol: IPPROTO_TCP,协议,如果输入0,可以根据第二个参数自动选择协议
返回值
socket,如果>0 就表示成功
*/
self.clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (self.clientSocket > 0) {
NSLog(@"创建成功 = %d",self.clientSocket);
} else {
NSLog(@"创建失败");
}
// 建立连接
/**
参数
1> 客户端socket
2> 指向数据结构sockaddr的指针,其中包括目的端口和IP地址。即服务器的“结构体”地址
3> 结构体数据长度
返回值
0 成功/其他 错误代号,非0即真
*/
struct sockaddr_in serverAddress;
// 设置ip--> 通过ip找主机
// inet_addr内部也会对ip地址进行字节翻转。就是高低互换
serverAddress.sin_addr.s_addr = inet_addr(host.UTF8String);
// 设置端口号--> 通过端口找程序
// htons函数会对传人的整数进行字节翻转。
serverAddress.sin_port = htons(port);
// 设置协议 --> 通过协议 确定如何传输数据
serverAddress.sin_family = AF_INET;
// C语言函数如果需要传递结构体的地址,一般都需要将结构体长度传递过去
return (connect(self.clientSocket, (const struct sockaddr *)&serverAddress, sizeof(serverAddress)) == 0);
}
/**
* 发送和接收
*/
- (NSString *)sendAndRecv:(NSString *)msg {
// 发送数据
/**
参数
1> 客户端socket
2> 发送内容地址 void * == id
3> 发送内容长度,是指字节的长度。
4> 发送方式标志,一般为0
返回值
如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR
*/
ssize_t sendLength = send(self.clientSocket, msg.UTF8String, strlen(msg.UTF8String), 0);
NSLog(@"发送了%ld长度的字节,%zd,%zd",sendLength,msg.length,strlen(msg.UTF8String));
// 读取数据
/**
参数
1> 客户端socket
2> 接受内容地址 void * == id
3> 接收内容长度,是指字节的长度。告诉服务器一次最多只能接收多少字节的内容。
4> 接收方式标志,0 代表阻塞式接收服务响应
返回值
如果成功,则返回接收的字节数,失败则返回SOCKET_ERROR
*/
unsigned char buffer[1024];
NSMutableData *dataM = [NSMutableData data];
ssize_t recvLength =-1;
while (recvLength != 0) {
// 循环接收服务返回的数据
recvLength = recv(self.clientSocket, buffer, sizeof(buffer), 0);
NSLog(@"接收了%ld长度的字节",recvLength);
// 拼接数据
[dataM appendBytes:buffer length:recvLength];
}
// 将二进制转换字符串
NSString *resultStr = [[NSString alloc] initWithData:dataM encoding:NSUTF8StringEncoding];
return resultStr;
}
/**
* 断开链接
*/
- (void)disconnection {
// 断开链接
close(self.clientSocket);
}
@end
友情提示
- ping 域名; 就能得到对应网站的ip地址。