函数指针,指针函数
函数指针是一个指向函数的指针,指针函数就说一个函数的返回值是一个指针。
函数指针的声明方法为:
函数类型 (标志符 指针变量名) (形参列表);注1:“函数类型”说明函数的返回类型,“(标志符 指针变量名 )”中的括号不能省,若省略整体则成为一个函数说明,说明了一个返回的数据类型是指针的函数,后面的“形参列表”表示指针变量指向的函数所带的参数列表。例如:
int func(int x); /* 声明一个函数 */
int (f) (int x); / 声明一个函数指针 */
f=func; /* 将func函数的首地址赋给指针f */
赋值时函数func不带括号,也不带参数,由于func代表函数的首地址,因此经过赋值以后,指针f就指向函数func(x)的代码的首地址。
不过注意,指向函数的指针变量没有++和–运算,用时要小心。
“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。如前所述,C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上一致的。函数指针有两个用途:调用函数和做函数的参数。- TCP UDP区别
1.链接:TCP面向链接,传输数据前要通过三次握手建立链接,结束时四次挥手结束链接。UDP是不可靠的传输,传输前不需要建立链接。
2.传输效率上:速度上,TCP速度慢,传输过程中需要校验数据,可以超时重发。UDP没有超时重发,数据校验等功能所以速度快。数据比例,TCP头至少20个字节,UDP头8个字节,相对效率高。
3.组装效率上:TCP头至少20个字节,UDP头8个字节,系统组装上TCP相对慢。
4.用途上:用于TCP可靠性,http,ftp使用。而由于UDP速度快,视频,在线游戏多用UDP,保证实时性。
5.TCP是一种流模式的协议,UDP是一种数据报模式的协议。
对于第五点的理解。TCP可能发送100个包,而接收到50个包,不是丢包了,而是每次接受的“包”都比发送的多,例如,每次发10个字节,可能读得时候一次读了20个字节。TCP是一种流模式的协议,在接收到的缓存中按照发送的包得顺序自动按照顺序拼接好,因为数据基本来自同一个主机,而且是按照顺序发送过来的,TCP的缓存中存放的就是,连续的数据。感觉好像是多封装了一步比UDP。而UDP因为可能两个不同的主机,给同一个主机发送,(一个端口可能收到多个应用程序的数据),或者按照TCP那样合并数据,必然会造成数据错误。我觉得关键的原因还是,TCP是面向连接,而UDP是无连接的,这就导致,TCP接收的数据为一个主机发来且有序无误的,而UDP可能是多个主机发来的无序,可能错误的。 - can’t create tcp/ip socket(24)暂时解决
mysql最大连接数,是个很简单的问题了,
vi /etc/my.cnf
在[mysqld]段中加入 max_connections=30000
这个can’t create tcp/ip socket(24)可能会倒是第二个问题
问题2.MySQL server has gone away
别的可能原因,长时间批量执行取得的mysql数据
也就是链接上了长时间没有操作
在my.ini文件中添加或者修改以下两个变量:
wait_timeout=2880000
interactive_timeout = 2880000 - 进程与线程区别
1.内核角度:进程是资源分配的最小单位,而线程是程序执行的最小单位。
2.地址空间上:进程有自己的独立的地址空间,一个进程崩溃不会对其他的进程产生影响,而一个线程没有自己的地址空间,一个线程死掉,整个进程就死掉了。
3.效率上:创建一个进程花费资源大(分配地址空间,建立数据表),所以切换时效率不高。而线程可以共享地址空间,共享大部分数据,所以创建快。
4.通信上:由于线程共享大部分数据,通信很方便,但要注意加锁。进程间通信麻烦,内存共享,消息队列,socket,管道。
5.多CPU:多个线程可以分别给不同的CPU来执行,可以很大提高效率。 - 设计模式-Proxy
一个类的用应经存在的类作为参数,这个存在的类最好是实现了某个接口,或者是个虚基类,这样参数可以灵活,充分利用了多态性。
以某个类作为参数,可以在原有类的基础上添加功能,如日志,而原来的类的功能直接用传进来的对象调用原来的函数(最好是没个接口的实现),用传进来的对象代理原对象的功能。
也可以以继承的方式实现聚合,但是,可能会因为要加的功能多从而造成类爆炸。而代理由于传进来的对象只要是某个虚基类的子类的对象,或者是实现了某个接口的类的对象都可以,所以灵活性较大,能充分提到代码的重复利用性。 - printf
int _main(int argc, _TCHAR* argv[])
{
float d=3.14;
printf("d=%.10e/n",d); //这里输出的6位以后的都是垃圾数据了,不准确
char ch[20];
strcpy(ch,"123456780123");
int n = 2,m = 10;
//*.* 呢,前边的*定义的是总的宽度,后边的定义的是输出的个数。
//如果后边的比前边的小,则使用空格在左侧补够m位。
printf("%*.*s/n",m,n,ch);//这里输出“ 12”
printf("%*.*s/n",n,m,ch);//这里输出“1234567890”
//补充,如果strlen(ch) < m的话,就输出strlen位 例如:
strcpy(ch, "1234678");
//这里结尾使用个hh作为输出,是为了更好的显示出输出12345678后到底光标到了那里
printf("%*.*shh/n",m,n,ch);//这里输出“ 12hh”
printf("%*.*shh/n",n,m,ch);//这里输出“12345678hh”
int y = 456;
//这里的#8d,保持宽度的,如果不够8位,就在左侧用空格补够
//如果超过8位,则有几位就输出几位。
printf("%#8d/n%#8x/n%#8o/n", y,y,y);
printf("%#3d/n%#3x/n%#3o/n", y,y,y);
printf("%#1d/n%#1x/n%#1o/n", y,y,y);
//这里的.8d估计大家都不陌生了吧?就是不够8位的时候左侧使用0补够
//同样,如果超过8位就有几位输出几位
printf("%.8d/n%.8x/n%.8o/n", y,y,y);
//这里的*d估计有的人有点陌生,其实可以看作是#6d,效果是一样的.
printf("%*d/n",6,y);
//这里的%+6d中的+号有俩意思:一、输出的数字前面有+号,二、不够6位左侧补空格
printf("%+6d/n",y);
//这连个和上面的%+6d的意思基本一样,但是如果y的位数+1没有6大,就用0补,但是个数是不超过
//6前面的0的个数。具体效果可以运行下看看
printf("%+006d/n",y);
printf("%+0006d/n",y);
//补充上面,如果6比y的位数小的话,只输出+号和y本身
//如果没有+号的话,则是使用0补充够6位 例如:
printf("%06d/n",y);//输出“000456”
//这里的-号是右侧补空格的意思 为了明显起见,我们仍旧使用hh作为结尾。
printf("%-6dhh/n",y);
//当然,如果这里的2没有y的位数大的话,就直接输出y,然后输出hh
//这里的-号仅仅是右侧补空格的意思
printf("%-2dhh/n",y);
//一个利用printf来输出的例子
int len = 0;
//这里%n的意思是将%n前的字符串的长度符给len:
//下面的例子是8 = strlen("hh") + strlen("123456");
printf("hh%s%n /n", "123456",&len);
printf("len=%d/n", len);
//本来不打算写他了 但是带上吧
//简单说明吧:.0f是小数点后0位,不带点 #.-0f就是带点 但是也是0位
//而%g则省略所以的无效的0 如果没有小数,则不带点 #g则一个0都不可以少!
printf("%.0fhh/n%#.0fhh/n%ghh/n%#ghh/n", 3.0,3.0,3.0,3.0);
//一个不明白的.这里《c陷阱与缺陷》中说输出7个空壳再输出%号。我试的怎么就一个%号。
printf("%*%/n", 8);
//好了,觉得不少了,如果不够了再给我发短信吧。
return 0;
}
7. 很多人对CRITICAL_SECTION的理解是错误的,认为CRITICAL_SECTION是锁定了资源,其实,CRITICAL_SECTION是不能够“锁定”资源的,它能够完成的功能,是同步不同线程的代码段。简单说,当一个线程执行了EnterCritialSection之后,cs里面的信息便被修改了,以指明哪一个线程占用了它。而此时,并没有任何资源被“锁定”。不管什么资源,其它线程都还是可以访问的(当然,执行的结果可能是错误的)。只不过,在这个线程尚未执行LeaveCriticalSection之前,其它线程碰到EnterCritialSection语句的话,就会处于等待状态,相当于线程被挂起了。 这种情况下,就起到了保护共享资源的作用。
也正由于CRITICAL_SECTION是这样发挥作用的,所以,必须把每一个线程中访问共享资源的语句都放在EnterCritialSection和LeaveCriticalSection之间。这是初学者很容易忽略的地方。
当然,上面说的都是对于同一个CRITICAL_SECTION而言的。 如果用到两个CRITICAL_SECTION,比如说:
第一个线程已经执行了EnterCriticalSection(&cs)并且还没有执行LeaveCriticalSection(&cs),这时另一个线程想要执行EnterCriticalSection(&cs2),这种情况是可以的(除非cs2已经被第三个线程抢先占用了)。 这也就是多个CRITICAL_SECTION实现同步的思想。
比如说我们定义了一个共享资源dwTime[100],两个线程ThreadFuncA和ThreadFuncB都对它进行读写操作。当我们想要保证dwTime[100]的操作完整性,即不希望写到一半的数据被另一个线程读取,那么用CRITICAL_SECTION来进行线程同步如下:
第一个线程函数:
DWORD WINAPI ThreadFuncA(LPVOID lp)
{
EnterCriticalSection(&cs);
...
// 操作dwTime
...
LeaveCriticalSection(&cs);
return 0;
}
写出这个函数之后,很多初学者都会错误地以为,此时cs对dwTime进行了锁定操作,dwTime处于cs的保护之中。一个“自然而然”的想法就是——cs和dwTime一一对应上了。 这么想,就大错特错了。dwTime并没有和任何东西对应,它仍然是任何其它线程都可以访问的。
如果你像如下的方式来写第二个线程,那么就会有问题:
DWORD WINAPI ThreadFuncB(LPVOID lp)
{
...
// 操作dwTime
...
return 0;
}
当线程ThreadFuncA执行了EnterCriticalSection(&cs),并开始操作dwTime[100]的时候,线程ThreadFuncB可能随时醒过来,也开始操作dwTime[100],这样,dwTime[100]中的数据就被破坏了。
为了让CRITICAL_SECTION发挥作用,我们必须在访问dwTime的任何一个地方都加上EnterCriticalSection(&cs)和LeaveCriticalSection(&cs)语句。所以,必须按照下面的方式来写第二个线程函数:
DWORD WINAPI ThreadFuncB(LPVOID lp)
{
EnterCriticalSection(&cs);
...
// 操作dwTime
...
LeaveCriticalSection(&cs);
return 0;
}
这样,当线程ThreadFuncB醒过来时,它遇到的第一个语句是EnterCriticalSection(&cs),这个语句将对cs变量进行访问。如果这个时候第一个线程仍然在操作dwTime[100],cs变量中包含的值将告诉第二个线程,已有其它线程占用了cs。因此,第二个线程的EnterCriticalSection(&cs)语句将不会返回,而处于挂起等待状态。直到第一个线程执行了LeaveCriticalSection(&cs),第二个线程的EnterCriticalSection(&cs)语句才会返回,并且继续执行下面的操作。
这个过程实际上是通过限制有且只有一个函数进入CriticalSection变量来实现代码段同步的。简单地说,对于同一个CRITICAL_SECTION,当一个线程执行了EnterCriticalSection而没有执行LeaveCriticalSection的时候,其它任何一个线程都无法完全执行EnterCriticalSection而不得不处于等待状态。
再次强调一次,没有任何资源被“锁定”,CRITICAL_SECTION这个东东不是针对于资源的,而是针对于不同线程间的代码段的!我们能够用它来进行所谓资源的“锁定”,其实是因为我们在任何访问共享资源的地方都加入了EnterCriticalSection和LeaveCriticalSection语句,使得同一时间只能够有一个线程的代码段访问到该共享资源而已(其它想访问该资源的代码段不得不等待)。
如果是两个CRITICAL_SECTION,就以此类推。
再举个极端的例子,可以帮助你理解CRITICAL_SECTION这个东东:
第一个线程函数:
DWORD WINAPI ThreadFuncA(LPVOID lp)
{
EnterCriticalSection(&cs);
for(int i=0;i <1000;i++)
Sleep(1000);
LeaveCriticalSection(&cs);
return 0;
}
第二个线程函数:
DWORD WINAPI ThreadFuncB(LPVOID lp)
{
EnterCriticalSection(&cs);
index=2;
LeaveCriticalSection(&cs);
return 0;
}
这种情况下,第一个线程中间总共Sleep了1000秒钟!它显然没有对任何资源进行什么“有意识”的保护;而第二个线程是要访问资源index的,但是由于第一个线程占用了cs,一直没有Leave,而导致第二个线程不得不登上1000秒钟……
第二个线程,真是可怜哪。。。
这个应该很说明问题了,你会看到第二个线程在1000秒钟之后开始执行index=2这个语句。
也就是说,CRITICAL_SECTION其实并不理会你关心的具体共享资源,它只按照自己的规律办事~
8. 日志函数
一、C语言编写的带时间标记的日志记录方法
#include <stdio.h>
#include "string.h"
#include "process.h"
#include <time.h>
#include <direct.h>//创建文件目录
void WriteLogMsg(char chLogMsg[])
{
time_t timeval;
timeval=time(NULL);//获取本地时间
tm tim=*localtime(&timeval);//得到相应的结构体,然后进行内容提取
char strFilePath[40] = "Log\\";//如果是"\\Log\\"则到了当前盘符的根目录下了。
char strTimeFileName[20];//将当前时间转换成字符串---声明字符串长度的时候,要比实际长度多1,作为结尾符号
strftime(strTimeFileName, sizeof(strTimeFileName), "%Y-%m-%d",&tim);//年月日字符串
strcat(strTimeFileName,".logFile");//加上扩展名--登录日志
strcat(strFilePath,strTimeFileName);//得到完整的路径名
FILE *fp;//文件指针
if ((fp=fopen(strFilePath,"a"))==NULL)//以追加的形式往文件中写东西
{
mkdir("Log");//如果在当前目录下没有打开,则重新创建新目录
if ((fp=fopen(strFilePath,"a"))==NULL)//以追加的形式往文件中写东西
{
printf("Open Failed\n");
exit(0);
}
}
char chTimeTag[20]; //将时间转成字符串
strftime(chTimeTag, sizeof(chTimeTag), "%Y/%m/%d %X",&tim);//年月日时间字符串--作为登录日志中信息的时间标记头
fputs(chTimeTag,fp);//写入时间标记
fputs(" : ",fp);//分隔符号
fputs(chLogMsg,fp);//写入消息日志
fputs("\n",fp);//换行
int i=fclose(fp);
if (i==0)
{
printf("succeed!\n");
}else
{
printf("fail!\n");
}
}
void main()
{
WriteLogMsg("Hello World!Zsm");
}
二、用C++编写的带时间标记的日志记录方法
void WriteLogMsg(char chLogMsg[])
{
char strFilePath[40] = "\\FlashDisk2\\Log\\";//如果是"\\Log\\"则到了当前盘符的根目录下了。
char strTimeFileName[20];//将当前时间转换成字符串---声明字符串长度的时候,要比实际长度多1,作为结尾符号
SYSTEMTIME sysTime;
GetLocalTime( &sysTime ); //得到系统时间
sprintf(strTimeFileName,"%d-%d-%d",sysTime.wYear,sysTime.wMonth,sysTime.wDay);//"2010-09-21"
strcat(strTimeFileName,".logFile");//加上扩展名--登录日志
strcat(strFilePath,strTimeFileName);//得到完整的路径名
FILE *fp;//文件指针
if ((fp=fopen(strFilePath,"a"))==NULL)//以追加的形式往文件中写东西
{
//如果打开不成功,则一般表示没有Log目录
//创建Log目录,然后再重新打开--一般情况下,如果目录存在的话,就不会创建成功的。
if(!CreateDirectory(_T("\\FlashDisk2\\Log"),NULL))
{
printf("Create Directory failed!\n");
}else
{
printf("Create Directory succeed!\n");//cout << "OK" <<endl;
if ((fp=fopen(strFilePath,"a"))==NULL)//以追加的形式往文本文件中写东西
{
printf("Open Failed\n");
exit(0);
}
}
}
char strTimeTag[30];//="2010-09-21"; //将时间转成字符串
sprintf(strTimeTag,"%d-%d-%d %d:%d:%d ",sysTime.wYear,sysTime.wMonth,sysTime.wDay,
sysTime.wHour,sysTime.wMinute,sysTime.wSecond);//"2010-09-21"
//strftime(chTimeTag, sizeof(chTimeTag), "%Y/%m/%d %X",&tim);//年月日时间字符串--作为登录日志中信息的时间标记头
fputs(strTimeTag,fp);//写入时间标记
fputs(" : ",fp);//分隔符号
fputs(chLogMsg,fp);//写入消息日志
fputs("\n",fp);//换行
int i=fclose(fp);
if (i==0)
{
printf("succeed!\n");
}else
{
printf("fail!\n");
}
}
9. fgets fputs
最近学习linux编程,熟悉了C语言标准IO库的一些常用函数,但在实践的时候还是遇到了一些问题。
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);
fgets函数从文件流stream中读出一个字符串,并将字符串写到s指向的字符串里。
函数在遇到一下三种情况时退出:
1、遇到换行
2、已经读取size-1个字符(不包括最后的"\0")
3、到达文件结尾
函数的返回值为指向s的指针,当文件流到达文件结尾,则返回空指针。
fputs函数向文件流中输出s所指的字符串,并且自动去掉字符串末尾的"\0"。
当成功完成时,返回一个非负整数;失败时返回EOF;
下面利用这两个函数实现一个文件复制程序:
#include <stdio.h>
#include <stdlib.h>
#define BLOCK 1024
int main(int argc,char *argv[])
{
FILE *in,*out;
char buf[BLOCK];
if(argc!=3){
fprintf(stderr,"Usage: %s filename1 filename2\n",argv[0]);
exit(1);
}
if((in=fopen(argv[1],"r"))==NULL){
fprintf(stderr,"Can not open file %s\n",argv[1]);
exit(1);
}
out=fopen(argv[2],"w");
while(fgets(buf,BLOCK,in)!=0)
fputs(buf,out);
fclose(in);
fclose(out);
exit(0);
}
这个程序可以实现在同一文件夹下的文件复制。在测试时发现,它只对文本文件有效,但在复制其他格式文件时会丢失数据。而之前我使用fgetc与fputc函数实现同样功能时没有出现这个问题。
我认为出现这个问题的原因在于fgets函数始终是以字符串的形式传输数据,对于非文本文件就会强制转换为文本文件而造成对数据的破坏。而fgetc函数是以int形传输,它对数据的原始状态进行了复制。
编写这个小程序的目的是想以块为单位复制,减少函数调用次数以达到提高效率的目的,但是显然fgets与fputs不能完成这个目标。
虽然还没达到预期的效果,但是还是对fgets、fputs有了更加深刻的理解
10. new malloc
摘自《高质量C++/C编程指南》
1.malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
2.对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构
函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
3.C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
4.C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存
总之:
new 是个操作符,和什么”+”,”-“,”=”…有一样的地位.
malloc是个分配内存的函数,供你调用的.
new是保留字,不需要头文件支持.
malloc需要头文件库函数支持.
new 建立的是一个对象,
malloc分配的是一块内存.
new建立的对象你可以把它当成一个普通的对象,用成员函数访问,不要直接访问它的地址空间
malloc分配的是一块内存区域,就用指针访问好了,而且还可以在里面移动指针.
11. C、C++一次将整个文件读入内存
@1.问题描述:
C和C++的初学者经常采用一行一行读入文件的办法对文件数据进行处理。但是经常会有一些情况需要将一个文件整体一次读入内存处理。而C和C++库中并没有提供直接一次读入文件全部数据的函数。
@2.解决方法:
目前给出C和C++的解决方案,下面两个程序只是用于演示,不过这些代码已经很容易改写成想要的函数了。
解决这个问题的思路是:
1.由于要将文件完整读入,所以必须使用二进制方式打开(若文本方式打开,文件流中会把一些非字符的数据过滤掉,我们将读取不到那些内容)。
2.打开文件后,我们首先获取文件的大小,然后在内存中分配足够的空间,再把文件拷贝到内存空间中。之后使用内存空间进行数据处理,演示程序中没有真正的处理,我们只是简单将其输出。
C实现
#include <stdio.h>
#include <stdlib.h>
int main ()
{
FILE * pFile;
long lSize;
char * buffer;
size_t result;
/* 若要一个byte不漏地读入整个文件,只能采用二进制方式打开 */
pFile = fopen ("test.txt", "rb" );
if (pFile==NULL)
{
fputs ("File error",stderr);
exit (1);
}
/* 获取文件大小 */
fseek (pFile , 0 , SEEK_END);
lSize = ftell (pFile);
rewind (pFile);
/* 分配内存存储整个文件 */
buffer = (char*) malloc (sizeof(char)*lSize);
if (buffer == NULL)
{
fputs ("Memory error",stderr);
exit (2);
}
/* 将文件拷贝到buffer中 */
result = fread (buffer,1,lSize,pFile);
if (result != lSize)
{
fputs ("Reading error",stderr);
exit (3);
}
/* 现在整个文件已经在buffer中,可由标准输出打印内容 */
printf("%s", buffer);
/* 结束演示,关闭文件并释放内存 */
fclose (pFile);
free (buffer);
return 0;
}
C++实现
#include <iostream>
#include <fstream>
using namespace std;
int main () {
filebuf *pbuf;
ifstream filestr;
long size;
char * buffer;
// 要读入整个文件,必须采用二进制打开
filestr.open ("test.txt", ios::binary);
// 获取filestr对应buffer对象的指针
pbuf=filestr.rdbuf();
// 调用buffer对象方法获取文件大小
size=pbuf->pubseekoff (0,ios::end,ios::in);
pbuf->pubseekpos (0,ios::in);
// 分配内存空间
buffer=new char[size];
// 获取文件内容
pbuf->sgetn (buffer,size);
filestr.close();
// 输出到标准输出
cout.write (buffer,size);
delete []buffer;
return 0;
}
12. TCP/IP
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接,如图1所示。
(1)第一次握手:建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。
(2)第二次握手:服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。
(3)第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。
完成三次握手,客户端与服务器开始传送数据。
图1 TCP三次握手建立连接
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。
(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
TCP采用四次挥手关闭连接如图2所示。
图2 TCP四次挥手关闭连接
1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。