TCP/UDP、socket

下面着重讲解TCP协议和UDP协议的区别。

TCP和UDP协议属于传输层协议。其中TCP提供IP环境下的数据可靠传输,它提供的服务包括数据流传送、可靠性、有效流控、全双工操作和多路复用。通过面向连接、端到端和可靠的数据包发 送。通俗说,它是事先为所发送的数据开辟出连接好的通道,然后再进行数据发送;而UDP则不为IP提供可靠性、流控或差错恢复功能。一般来说,TCP对应 的是可靠性要求高的应用,而UDP对应的则是可靠性要求低、传输经济的应用。TCP支持的应用协议主要有:Telnet、FTP、SMTP等;UDP支持 的应用层协议主要有:NFS(网络文件系统)、SNMP(简单网络管理协议)、DNS(主域名称系统)、TFTP(通用文件传输协议)等。

一、面向连接的TCP

     “面向连接”就是在正式通信前必须要与对方建立起连接。比如你给别人打电话,必须等线路接通了、对方拿起话筒才能相互通话。

      TCP是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。一个TCP连接必须要经过三次“对话”才能建立起来,其中这三次对话的简单过程:主机A向主机B发出连接请求数据包:“我想给你发数据,可以吗?”,这是第一次对话;主机B向主机A 发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话;主机A再发出一个数 据包确认主机B的要求同步:“我现在就发,你接着吧!”,这是第三次对话。三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据。

      TCP协议能为应用程序提供可靠的通信连接,使一台计算机发出的字节流无差错地发往网络上的其他计算机,对可靠性要求高的数据通信系统往往使用TCP协议传输数据。


二、连接建立

    TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK ,并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP使用的流量控制协议是可变大小的滑动窗口协议。 TCP三次握手的过程如下,具体如图2-9所示。


  • TCP的三次握手


     图2-9是TCP三次握手的过程,具体步骤如下:

  1. 客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
  2. 服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
  3. 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。

 通过上述步骤三次握手完成了,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。

三、连接终止

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。

  • TCP的连接的终止


图2-8是TCP连接的终止过程,具体步骤分析如下:

(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。

(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。

注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。

(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。

(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。

既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。

注意:

(1) “通常”是指,某些情况下,步骤1的FIN随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。

(2) 在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为“半关闭”(half-close)。

(3) 当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。

无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。

四、面向非连接的UDP协议

    “面向非连接”就是在正式通信前不必与对方先建立连接,不管对方状态就直接发送。与手机短信非常相似:你在发短信的时候,只需要输入对方手机号就OK了。

UDP是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!

UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境。比如,我们经常使用“ping”命令来测试两台主机之 间TCP/IP通信是否正常,其实“ping”命令的原理就是向对方主机发送ICMP数据包,然后对方主机确认收到数据包,如果数据包是否到达的消息及时 反馈回来,那么网络就是通的。例如,在默认状态下,一次“ping”操作发送4个数据包(如图所示)。大家可以看到,发送的数据包数量是4包,收到的也是 4包(因为对方主机收到后会发回一个确认收到的数据包)。这充分说明了UDP协议是面向非连接的协议,没有建立连接的过程。正因为UDP协议没有连接的过程,所以它的通信效率高;但也正因为如此,它的可靠性不如TCP协议高。QQ就使用UDP发消息,因此有时会出现收不到消息的情况。

主要区别如表2-1所示。

  1. TCP协议和UDP协议的区别

   从表2-3可知,TCP协议和UDP协议各有所长、各有所短,适用于不同要求的通信环境。



  1. Socket

TCP/IP是传输层协议,主要解决数据如何在网络中传输;Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

 Socket又称"套接字",Socket开发是纯c语言的,跨平台的。应用程序通常通过"套接字"向网络发出请求或者应答网络请求。其中Socket也是为网络服务提供一种机制,网络通信其实就是socket之间的通信,数据在两个socket之间通过io传输。http基于socket,http底层使用的是tcp协议。具体如图2-6所示。 

  • Socket通讯示意图

图2-5 左边是客户端,右边是服务端。开发主要是客户端的实现。首先是实例化一个socket,connect是连接一个服务器,write是给服务器发送数据,发完数据后等待服务器响应,然后read即recv,读取数据。Write和red可以循环一直执行,等整个读写完毕之后就执行close关闭,和服务器断开连接。

为了让大家更好地理解Socket的编程,接下来,创建一个Single View Application应用,命名为Socket编程体验,进入viewController.m文件,下面是网络socket基本演练。具体步骤如下所示。


1、导入头文件

    #import <sys/socket.h>

    #import <netinet/in.h>

    #import <arpa/inet.h>

因为Socket是纯C语言的,在编写程序之前要导入三个头文件,这三个头文件是客户端为开发前准备的,是固定的。


2、定义属性

    @interface ViewController ()

    // 客户端 socket

    @property (nonatomic, assign) int clientSocket;

    @end

从上述代码可以看出,clientSocket是为客户端socket添加的一个属性。


3、建立 socket

    self.clientSocket = socket(AF_INET, SOCK_STREAM, 0);

    NSLog(@"%d", self.clientSocket);

     建立客户端socket连接,可以参考Socket通讯示意图来帮助我们梳理思路。从上述代码可以看出socket里面包含了三个参数, 其函数原型为:int socket(int domain,int type, int protocol),接下来我们给其中的三个参数做详细的说明,具体如下。

  • domain:协议域,又称协议族(family)。常用的协议族有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等。流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。
  • protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

    注意:1.type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。

          2.WindowsSocket下protocol参数中不存在IPPROTO_STCP


    4、连接到主机

    struct sockaddr_in serverAddr;

    serverAddr.sin_family = AF_INET;

    serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    serverAddr.sin_port = htons(12345);

return connect(self.clientSocket, (const struct sockaddr *)&serverAddr, 

sizeof(serverAddr)) == 0;          

从上述代码可以看出,sockaddr_in serverAddr代表服务器地址的结构体,_in 专门用来做互联网机器的地址的结构体。Family指用的哪一个协议 。addr 是主机地址,inet_addr函数将字符串的ip地址转换成数字。也就是指IP地址。port 指连接主机的时候需要的端口,端口号不要低于1024,可以自己指定。connect()用于建立于指定socket的连接。函数原型: int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR* name, int namelen);

    参数:

  • s:标识一个未连接socket
  • name:指向要连接套接字的sockaddr结构体的指针
  • namelen:sockaddr结构体的字节长度


    5、调用连接到主机

    if (![self connectToHost]) {

        NSLog(@"失败");

        return;

    }

    NSLog(@"成功")

从上述代码可以看出,判断是否连接成功,非零即真,函数执行返回0,就是正确的,返回其他的就是错误的。  

      6、发送消息

    NSString *msg = @"约?";

    ssize_t sendLen = send(clientSocket, msg.UTF8String, 

    strlen(msg.UTF8String),0);

    NSLog(@"发送 %ld %tu %ld", sendLen, msg.length, strlen(msg.UTF8String));

从上述代码可以看出,发送数据里面包含三个参数,其函数原型为:send(int, const,void *, size_t, int),其中第一个参数为客户端socket,第二个参数为发送内容地址,都是二进制数据。第三个参数为发送内容和长度。第四个参数为发送方式标志,一般为0。

     7、接收消息

    uint8_t buffer[1024];

    ssize_t recvLen = recv(self.clientSocket, buffer, sizeof(buffer), 0);

    NSLog(@"接收了 %ld %ld", recvLen, sizeof(buffer));

    NSString *result = [[NSString alloc] initWithBytes:buffer length:recvLen

    encoding:NSUTF8StringEncoding];

    return result; 

     从上述代码可以看出,uint8_t buffer[1024]为准备的缓冲区,8位一组,长度是1024的无符号的字符串数组。接收数据里面包含三个参数,函数原型为recv(int, void *, size_t, int),第一个参数为客户端socket,第二个参数为接收内容缓冲地址,第三个参数为接收内容缓冲区长度,第四个参数为接收方式,0表示阻塞,必须等待服务器返回数据。最后NSUTF8StringEncoding编码为字符串的转换。返回值如果成功,则返回读入的字节数,失败则返回SOCKET_ERROR。

    8、断开连接

    - (void)disconnect {

        close(self.clientSocket);

    }

从上述代码可以看出,给服务器发送完数据,服务器回应数据完成后,需要关闭连接。

   

  1. 实战演练—socket聊天

了解了socket的基本流程,为了大家更深入地掌握,接下来我们在上面的基础上实现一个socket聊天。


  一、创建工程

   (1)新建一个Single View Application应用,命名01_Socket聊天。

  二、搭建界面

(1)在故事板界面中添加两个Label,用于输入地址(127.0.0.1)和端口号(12345)再添加一个连接按钮、一个TextField,用于输入发送的内容、一个发送按钮、一个Label,表示从服务端接收回来的内容。

(2)如果屏幕的尺寸改变的话,UI元素的位置和大小也需要相应的做出改变,这里需要使用自动布局(Auto Layout),这是iOS6为iPhone和iPad带来了一个极好的新特性,使app能在各种尺寸的屏幕上都能自适应,即不用代码来判断屏幕大小来设置view的大小。在平面直角坐标系中,要准确描述一个视图位置需要确定以下四个布局属性(Layout Attribute),即水平位置x(左侧)、垂直位置y(顶部)、宽度w、高度h(暂不考虑旋转)。只有上述4个布局属性都明确的情况下,该视图才能正确添加到界面中。

(3)下面我们来简单介绍自动布局的使用。首先我们选中要添加约束的控件,点击故事板右下角的第二个小面板菜单来做constraint。添加完要约束的属性后,点击“Add Constraints”按钮完成约束,具体如图2-7所示。

  • 约束控件

故事板中所有的控件约束的属性分别为:约束地址(左侧,顶部,宽,高)、约束端口号(左侧,顶部,宽,高)、连接按钮(左侧,顶部,宽,高)。为了让文本框能够根据屏幕的宽高自动适应,先对发送按钮进行约束(右侧,顶部,宽,高)、文本框(顶部,左侧,右侧,高)、Label(顶部,左侧,右侧,高)。


三、创建对象的关联

(1)点击点击Xcode 6.1界面右上角的图标,进入控件与代码的关联界面,采用控件和代码关联的方式,为地址(127.0.0.1)、端口号(12345)、连接按钮、TextField、发送按钮、Label从服务端接收回来的内容添加属性,为两个按钮添加点击事件,添加完成的具体代码如下所示:

  1.     #import "ViewController.h"
  2.     #import <sys/socket.h>
  3.     #import <netinet/in.h>
  4.     #import <arpa/inet.h>
  5.     @interface ViewController ()
  6.     //主机地址
  7.     @property (weak, nonatomic) IBOutlet UITextField *hostText;
  8.      //端口号
  9.     @property (weak, nonatomic) IBOutlet UITextField *portText;
  10.     //发送信息文字
  11.     @property (weak, nonatomic) IBOutlet UITextField *msgText;
  12.     //接收文字标签
  13.     @property (weak, nonatomic) IBOutlet UILabel *recvLabel;
  14.     /** 客户端 socket */
  15.     @property (nonatomic, assign) int clientSocket;
  16.     @end
  17.     @implementation ViewController
  18.     /**
  19.      * 连接按钮的点击
  20.      */
  21.     - (IBAction)conn {
  22.        self.recvLabel.text = [self connectToHost:self.hostText.text 
  23.     port:self.portText.text.intValue];
  24.         }
  25.     /**
  26.      * 发送按钮的点击
  27.      */



四、调整连接到主机地址,添加参数。

  1.     - (IBAction)send {
  2.         self.recvLabel.text = [self chat:self.msgText.text];
  3.         }
  4.     - (NSString *)connectToHost:(NSString *)host port:(int)port {
  5.         // 1. 客户端的socket 
  6.         self.clientSocket = socket(AF_INET, SOCK_STREAM, 0); 
  7.         // 2. 连接服务器
  8.         struct sockaddr_in serverAddr;
  9.         serverAddr.sin_family = AF_INET;
  10.         // 端口号不要低于1024,可以自己指定
  11.         serverAddr.sin_port = htons(port);
  12.         // 主机地址 inet_addr函数将字符串的ip地址转换成数字
  13.         serverAddr.sin_addr.s_addr = inet_addr(host.UTF8String);
  14.         int connectResult = connect(self.clientSocket, (const struct sockaddr 
  15.         *)&serverAddr, sizeof(serverAddr));
  16.         if (connectResult) {
  17.             NSLog(@"失败 %d", connectResult);
  18.             return @"无法连接";
  19.             } else {
  20.             NSLog(@"OK");  
  21.             return @"OK";
  22.             }
  23.         }

实现连接功能,判断是否连接成功,非零即真,函数执行返回0,就是正确的,返回其它的就是错误的。


五、调整发送和接收方法,添加参数,将数据转换成字符串格式。

  1.         // MARK: 聊天
  2.         - (NSString *)chat:(NSString *)msg {
  3.         // 3. 发送数据
  4.         // NSString = @"abc" 保存在常量区
  5.         const char *sendMsg = [msg UTF8String];
  6.      ssize_t sendLen = send(self.clientSocket, sendMsg, strlen(sendMsg),  0);
  7.         NSLog(@"%ld", sendLen);
  8.         // 4. 接收数据
  9.         // 长度是1024的无符号的字符串数组
  10.         uint8_t buffer[1024];
  11.        ssize_t recvLen = recv(self.clientSocket, &buffer, sizeof(buffer), 0);
  12.         NSLog(@"%ld", recvLen);
  13.         // 转换数据=> NSData
  14.         NSData *data = [NSData dataWithBytes:&buffer length:recvLen];
  15.         // 转换成字符串
  16.         NSString *str = [[NSString alloc] initWithData:data 
  17.         encoding:NSUTF8StringEncoding];
  18.         return str;
  19.         }
  20.         // MARK: 关闭
  21.         - (void)closeSocket {
  22.         // 5. 关闭连接
  23.         close(self.clientSocket);
  24.         }

从上述代码可以看出,在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

socket访问需要使用请求,在HTTP通信过程中,协议规定一个完整的由客户端发送给服务器的请求包含请求行、请求头、请求体。具体代码如下。


  1.     // HTTP端口默认是80,url中的 http,其实一个含义表示 80
  2.     // 主机地址,目前应该用ip地址
  3.     // 百度的ip 220.181.57.216
  4.     NSLog(@"%@", [self connectToHost:@"127.0.0.1" port:80]);
  5.     // 发送数据给百度的服务器,特殊的程序,就需要按照规矩来->HTTP协议
  6.     // 发送给服务器的请求,从firebug拦截到的(请求头)
  7.     NSString *request = @"GET / HTTP/1.1\n"
  8.     "Host: localhost\n"
  9.     "User-Agent: iPhone\n"
  10.     "Accept: 
  11.     text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n"
  12.     "Accept-Language: zh-cn\n"
  13.     "Accept-Encoding: gzip, deflate\n"
  14.     "Connection: keep-alive\n\n";
  15.     [self chat:request];

例2-4 代码第7行为请求行,包含了请求方法、请求资源路径、HTTP协议版本。第8行为客户端想访问的服务器主机的地址。第9~14行,User-Agent为客户端的类型,客户端的软件环境。Accept用于高速服务器,客户端所能接收的数据类型。Accept-Language为客户端的语言环境。Accept-Encoding为客户端支持的数据压缩格式。Connection为连接。在每个请求头的末尾都加上一个”\n”,最后一个Connection用两个。以上拼接字符串的格式缺一不可。此协议是客户端与服务器双方约定好的规则。

打印器返回的信息如图2-5所示。

  • 服务器的响应


   图2-4为发送请求后,服务器给我们的一个响应。内容具体解析如下:

  • 状态行:包含了HTTP协议版本、状态码、状态英文名称

HTTP/1.1 200 OK

  • 响应头:包含了对服务器的描述、对返回数据的描述

Date:Sun,28 Dec 2014 07:40:46 GMT     // 响应的时间

Server:Apache-Coyote/1.1            // 服务器的类型

Content – Length:44                // 返回数据的长度

Content-Type: text/html            // 返回数据的类型

  • 实体内容:服务器返回给客户端的具体数据,比如文件数据

<html><body><h1>It works!</h1></body></html>这是服务器返回的真正数据内容

    以上为完整的HTTP通讯的协议,接下来,通过一张演示图概括整个流程,如图2-5所示。

  •  HTTP通信演示图


   在表2-3中,HTTP状态码(HTTP Status Code)是用以表示网页服务器HTTP响应状态的3位数字代码。状态代码(也称作错误代码),指为服务器所接收每个请求(网页点击)分配的 3 位数代码。多数有效网页点击都有状态代码 200("正常")。"网页未找到"错误会生产 404 错误。某些常见的代码以粗体显示。具体如表所示:



  1. 常见响应状态码


   所有状态码的第一个数字代表了响应的五种状态之一。

  • 1xx(临时响应)

    用于表示临时响应并需要请求者执行操作才能继续的状态代码。

  • 2xx(成功)

    用于表示服务器已成功处理了请求的状态代码。

  • 3xx(已重定向)

    要完成请求,需要进一步进行操作。通常,这些状态代码是永远重定向的。Google 建议您在每次请求时使用的重定向要少于 5 个。您可以使用网站管理员工具来查看 Googlebot 在抓取您已重定向的网页时是否会遇到问题。诊断下的抓取错误页中列出了 Googlebot 由于重定向错误而无法抓取的网址。

  • 4xx(请求错误)

    这些状态代码表示,请求可能出错,已妨碍了服务器对请求的处理。

  • 5xx(服务器错误)

    这些状态代码表示,服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值