1、TCP协议的三次握手和四次挥手
2、SOCKET连接与TCP连接
3、SOCKET详解
3.1 socket套接字的起源
3.2 套接字描述符
3.3. SOCKET连接的程序
4、TCP/IP.Http,Socket关系研究,TCP和UDP的区别
1、TCP协议的三次握手和四次挥手
TCP协议的三次握手和四次挥手是很经典的内容
内容来源于
http://blog.csdn.net/whuslei/article/details/6667471
http://blog.csdn.net/zeng622peng/article/details/5546384
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
下图描述了如何握手
那么,如何“挥手”告别呢?
【注意】中断连接端可以是Client端,也可以是Server端。
[1]假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。
[2]所以Server先发送ACK,"告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息"。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。
[3]当Server端确定数据已发送完成,则向Client端发送FIN报文,"告诉Client端,好了,我这边数据发完了,准备好关闭连接了"。Client端收到FIN报文后,"就知道可以关闭连接了,
[4]但是Client还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,"就知道可以断开连接了"。Client端等待了2MSL(最大报文段生存时间)后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!
TIME_WAIT状态中所需要的时间是依赖于实现方法的。典型的值为30秒、1分钟和2分钟。等待之后连接正式关闭,并且所有的资源(包括端口号)都被释放。
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
上文出现了Socket连接,那么Socket连接与TCP连接什么关系呢?
2、SOCKET连接与TCP连接
创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。
3、讲完了相互关系,那么Socket是个什么东西呢?
首先,跑个题。方便后期理解,先讲讲起源。
内容摘自:http://blog.csdn.net/hguisu/article/details/7445768/
进程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如
UNIX BSD有:管道(pipe)、命名管道(named pipe)软中断信号(signal)
UNIX system V有:消息(message)、共享存储区(shared memory)和信号量(semaphore)等.
他们都仅限于用在本机进程之间通信。网间进程通信要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。为此,首先要解决的是网间进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。 其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。
其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。
这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是为什么说“一切皆socket”。
3.1 socket套接字的起源
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭).
说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
注意:其实socket也没有层的概念,它只是一个门面模式(facade模式)的应用,让编程变的更简单。是一个软件抽象层。在网络编程中,我们大量用的都是通过socket实现的。
3.2 套接字描述符
其实就是一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr
套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起,所以应用程序可以对文件进行套接字I/O或I/O读/写操作。
当应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字,应用程序则使用这个描述符来引用该套接字需要I/O请求的应用程序请求操作系统打开一个文件。
操作系就创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。下图显示,操作系统如何把文件描述符实现为一个指针数组,这些指针指向内部数据结构。
对于每个程序,系统都有一张单独的表。精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者(进程) 。
应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。
针对套接字的系统数据结构:(套接字类似于文件,也有自己对应的内部数据结构)
1)、套接字API里有个函数socket,它就是用来创建一个套接字。套接字设计的总体思路是,单个系统调用就可以创建任何套接字,因为套接字是相当笼统的。一旦套接字创建后,应用程序还需要调用其他函数来指定具体细节。例如调用socket将创建一个新的描述符条目:
2)、虽然套接字的内部数据结构包含很多字段,但是系统创建套接字后,大多数字字段没有填写。应用程序创建套接字后在该套接字可以使用之前,必须调用其他的过程来填充这些字段。
插播一下:文件描述符和文件指针的区别:
文件描述符:在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。
文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。
3.3. SOCKET连接的程序
在生活中,A要电话给B,A拨号,B听到电话铃声后提起电话,这时A和B就建立起了连接,A和B就可以讲话了。等交流结束,挂断电话结束此次交谈。 打电话很简单解释了这工作原理:“open—write/read—close”模式。
1、服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
2、在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
3、客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,
4、最后关闭连接,一次交互结束。
首先初始化Socket,其实就是定义一个变量,往里面配置参数。
这边使用的是 结构体 sockaddr_in来添加地址也就是参数(在netinet/in.h(Linux)中定义)
摘自:http://baike.baidu.com/link?url=BzyfWErcb2pbQkmMtEX6l8xWq1QaN0npcQ1HxNd0zlpNSRVqhf7WvZyyhOBOKL_wlV4cXNtECw9J1wqmaYmGZa
struct sockaddr_in { short sin_family;/*Addressfamily一般来说AF_INET(地址族)PF_INET(协议族)*/ unsigned short sin_port;/*Portnumber(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)*/ struct in_addr sin_addr;/*Internetaddress*/ unsigned char sin_zero[8];/*Samesizeasstructsockaddr没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐*/ }; struct in_addr其实就是32位IP地址 struct in_addr { unsigned long s_addr; };
还有一个类似的结构体是sockaddr,网上有很多关于两者的比较,比如http://blog.csdn.net/joeblackzqq/article/details/8258693
二者的占用的内存大小是一致的,因此可以互相转化,从这个意义上说,他们并无区别。
sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息。是一种通用的套接字地址。而sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作。使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。
看一下服务器的程序:
/* File Name: server.c */ #include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include <arpa/inet.h> // 在原来的.c后缀名源文件基础上加的头文件 linaijun #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #define DEFAULT_PORT 8000 #define MAXLINE 4096 int main(int argc, char** argv) { int socket_fd, connect_fd; struct sockaddr_in servaddr; char buff[4096]; int n; //Step1:初始化Socket变量;初始化Socket对应的地址变量,写入参数 //对应上文所述的,一旦套接字创建后,应用程序还需要调用其他函数来指定具体细节 // 初始化Socket if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ) { printf("create socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } // 初始化 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。 servaddr.sin_port = htons(DEFAULT_PORT); // 设置的端口为DEFAULT_PORT //Step2:将地址绑定在Socket的文件描述符上 // 将本地地址绑定到所创建的套接字上 if (bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) { printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } //Step3::listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。 //在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。 // 开始监听是否有客户端连接 if (listen(socket_fd, 10) == -1) { printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } printf("======waiting for client‘s request======\n"); while (1) { //Step4:等待着客户端的连接 // 阻塞直到有客户端连接,不然多浪费CPU资源。 if ((connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1) { printf("accept socket error: %s(errno: %d)",strerror(errno),errno); continue; } //Step5:连接完毕,开始通讯,直到关闭 // 接受客户端传过来的数据 n = recv(connect_fd, buff, MAXLINE, 0); // 向客户端发送回应数据 if (!fork()) { /* 连接成功 */ if (send(connect_fd, "Hello,you are connected!\n", 26,0) == -1) perror("send error"); close(connect_fd); exit(0); } buff[n] = ‘\0‘; printf("recv msg from client: %s\n", buff); close(connect_fd); } close(socket_fd); }
流程为:
Socket()//Step1:初始化Socket变量;初始化Socket对应的地址变量,写入参数
Blind()//Step2:将地址绑定在Socket的文件描述符上
Listen()//Step3::listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。 //在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。
Accept()//Step4:等待着客户端的连接
Read()Write()Close()//Step5:连接完毕,开始通讯,直到关闭 而客户端的程序:
/* File Name: client.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> // 在原来的.c后缀名源文件基础上加的头文件 linaijun #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #define MAXLINE 4096 #include <iostream> using namespace std; int main(int argc, char** argv) { int sockfd, n,rec_len; char recvline[4096], sendline[4096]; char buf[MAXLINE]; struct sockaddr_in servaddr; if (argc != 2) { printf("usage: ./client <ipaddress>\n"); exit(0); } //Step1:新建Socket if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("create socket error: %s(errno: %d)\n", strerror(errno),errno); exit(0); } //*************add family,IP address,Port number into seraddr memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8000); if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) { printf("inet_pton error for %s\n",argv[1]); exit(0); } //End*************add family,IP address,Port number into seraddr //Step2:将socket连接到服务器的地址上 if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) { printf("connect error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } //Step3:读写操作直到结束 printf("send msg to server: \n"); fgets(sendline, 4096, stdin); if (send(sockfd, sendline, strlen(sendline), 0) < 0) { printf("send msg error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } if ((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) { perror("recv error"); exit(1); } buf[rec_len] = ‘\0‘; printf("Received : %s ",buf); close(sockfd); exit(0); }
流程为:
Socket()//Step1:新建Socket
Connect()//Step2:将socket连接到服务器的地址上
Read()Write()Close()//Step3:读写操作直到结束
阅读两部分程序,发现一开始都会新建一个Socket(获得一个Socket描述符sockfd ),初始化一个地址变量,然后写入地址
//【Step1】建立Socket //对应上文所述的,一旦套接字创建后,应用程序还需要调用其他函数来指定具体细节 // 初始化Socket if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ) { printf("create socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } // 初始化 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。 servaddr.sin_port = htons(DEFAULT_PORT); // 设置的端口为DEFAULT_PORT //Step2:将地址绑定在Socket的文件描述符上 // 将本地地址绑定到所创建的套接字上 if (bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) { printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } |
//Step1:新建Socket if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("create socket error: %s(errno: %d)\n", strerror(errno),errno); exit(0); } //*************add family,IP address,Port number into seraddr memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8000); if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) { printf("inet_pton error for %s\n",argv[1]); exit(0); } //End*************add family,IP address,Port number into seraddr //Step2:将socket连接到服务器的地址上 if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) { printf("connect error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } |
两者的不同之处在于,服务器是将自己的Socket绑定(Bind)到这个地址上,那么以后连接这个地址的Socket,就连接到服务器的Socket了
客户端的Socket不需要绑定这个地址(这个地址也不是它的),直接去连接这个地址就行了。(也就是Connect)
好了,说完程序,运行一下看看
4、讲完了Socket,那么Socket,tcp/ip,udp,http到底什么关系呢?
1、基本概念
网络由下往上分为
物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
IP协议对应于网络层,TCP协议对应于传输层,而HTTP协议对应于应用层,三者从本质上来说没有可比性,
socket则是对TCP/IP协议的封装和应用(程序员层面上)。也可以说,TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。
2、TCP/IP和HTTP协议的关系
【关于TCP/IP和HTTP协议的关系,网络有一段比较容易理解的介绍】:
“我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容。
如果想要使传输的数据有意义,则必须使用到应用层协议。
应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。
WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。”
3、socket和TCP/IP协议的关系
而我们平时说的最多的socket是什么呢,实际上socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API)。通过Socket,我们才能使用TCP/IP协议。
实际上,Socket跟TCP/IP协议没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、listen、connect、accept、send、read和write等等。
【网络有一段关于socket和TCP/IP协议关系的说法比较容易理解】:
“TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。这个就像操作系统会提供标准的编程接口,比如win32编程接口一样,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。”
4、http和Socket的关系
【关于HTTP和Socket,CSDN上有个比较形象的描述:】
HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
实际上,传输层的TCP是基于网络层的IP协议的,而应用层的HTTP协议又是基于传输层的TCP协议的,而Socket本身不算是协议,就像上面所说,它只是提供了一个针对TCP或者UDP编程的接口。
5、TCP和UDP的区别
问题来了,TCP和UDP的区别是什么?
5.1、TCP是面向链接的,虽然说网络的不安全不稳定特性决定了多少次握手都不能保证连接的可靠性,但TCP的三次握手在最低限度上(实际上也很大程度上保证了)保证了连接的可靠性;
而UDP不是面向连接的,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,当然也不用重发,所以说UDP是无连接的、不可靠的一种数据传输协议。
5.2、也正由于1所说的特点,使得UDP的开销更小、数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。
知道了TCP和UDP的区别,就不难理解为何采用TCP传输协议的MSN比采用UDP的QQ传输文件慢了,但并不能说QQ的通信(使用UDP)是不安全的,
因为程序员可以手动对UDP的数据收发进行验证,比如发送方对每个数据包进行编号然后由接收方进行验证啊什么的,
即使是这样,UDP因为在底层协议的封装上没有采用类似TCP的“三次握手”而实现了TCP所无法达到的传输效率。