五年前随笔(二)

  1. 函数指针,指针函数
    函数指针是一个指向函数的指针,指针函数就说一个函数的返回值是一个指针。
    函数指针的声明方法为:
      函数类型 (标志符 指针变量名) (形参列表);

      注1:“函数类型”说明函数的返回类型,“(标志符 指针变量名 )”中的括号不能省,若省略整体则成为一个函数说明,说明了一个返回的数据类型是指针的函数,后面的“形参列表”表示指针变量指向的函数所带的参数列表。例如:

      int func(int x); /* 声明一个函数 */

      int (f) (int x); / 声明一个函数指针 */

      f=func; /* 将func函数的首地址赋给指针f */

      赋值时函数func不带括号,也不带参数,由于func代表函数的首地址,因此经过赋值以后,指针f就指向函数func(x)的代码的首地址。
    不过注意,指向函数的指针变量没有++和–运算,用时要小心。
    “函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。如前所述,C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
    有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上一致的。函数指针有两个用途:调用函数和做函数的参数。

  2. 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可能是多个主机发来的无序,可能错误的。
  3. 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
  4. 进程与线程区别
    1.内核角度:进程是资源分配的最小单位,而线程是程序执行的最小单位。
    2.地址空间上:进程有自己的独立的地址空间,一个进程崩溃不会对其他的进程产生影响,而一个线程没有自己的地址空间,一个线程死掉,整个进程就死掉了。
    3.效率上:创建一个进程花费资源大(分配地址空间,建立数据表),所以切换时效率不高。而线程可以共享地址空间,共享大部分数据,所以创建快。
    4.通信上:由于线程共享大部分数据,通信很方便,但要注意加锁。进程间通信麻烦,内存共享,消息队列,socket,管道。
    5.多CPU:多个线程可以分别给不同的CPU来执行,可以很大提高效率。
  5. 设计模式-Proxy
    一个类的用应经存在的类作为参数,这个存在的类最好是实现了某个接口,或者是个虚基类,这样参数可以灵活,充分利用了多态性。
    以某个类作为参数,可以在原有类的基础上添加功能,如日志,而原来的类的功能直接用传进来的对象调用原来的函数(最好是没个接口的实现),用传进来的对象代理原对象的功能。
    也可以以继承的方式实现聚合,但是,可能会因为要加的功能多从而造成类爆炸。而代理由于传进来的对象只要是某个虚基类的子类的对象,或者是实现了某个接口的类的对象都可以,所以灵活性较大,能充分提到代码的重复利用性。
  6. 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报文。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值