目录:
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) ",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) ",strerror(errno),errno); exit(0); } //Step3::listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。 //在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。 // 开始监听是否有客户端连接 if (listen(socket_fd, 10) == -1) { printf("listen socket error: %s(errno: %d) ",strerror(errno),errno); exit(0); } printf("======waiting for client's request====== "); 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! ", 26,0) == -1) perror("send error"); close(connect_fd); exit(0); } buff[n] = '