Linux网络编程(二)TCP协议通信

来源:互联网 发布:sql 2005卸载 编辑:程序博客网 时间:2024/05/21 11:01

一、通讯流程:
1、服务器调用socket()、bind()、listen()、完成初始化后,调用accept()阻塞式等待客户端连接,此时处于监听状态
2、客户端调用socket()初始化,调用connect()函数发出SYN请求并阻塞式等待服务器应答。
3、服务器应答一个SYN+ACK段,客户端收到之后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回(三次握手过程)

这里写图片描述

二、数据传输过程:
建立连接后,TCP协议提供全双工的通信服务,但是一般情况下,都是客户端主动发起请求,服务器被动处理请求,采用一问一答模式,因此,服务器从accept()返回之后就立刻调用read(),读取数据,如果没有数据,就处于阻塞状态等待数据来临,客户端调用write()函数发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器应答,服务器调用write()将处理结果返回给客户端,再次循环处理请求。。。。。

如果客户端没有更多的请求,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()就会返回0,这样服务器就知道了客户端关闭了连接,也会调close()关闭连接。注意:任何一方调用close()关闭,双方都会关闭连接,不能再通信,但是如果有一方调用shutdown(),则处于半关闭状态,仍可接收对方发来的数据。
代码如下 :

servre.c#include <stdio.h>#include <sys/types.h>#include  <sys/socket.h>#include <string.h>#include <stdlib.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>int main(int argc,char* argv[]){    if(argc != 3)    {        printf("Usage  : %s[local_ip] [local_port]\n",argv[0]);        return -1;    }    //创建socket ,获得监听套结字(服务器套结字)    int listen_fd;    listen_fd = socket(AF_INET,SOCK_STREAM,0);    if(listen_fd<0)    {        perror("touch listen_fd fail\n");        return -1;    }    printf("创建listen_fd_____%d\n",listen_fd);    //设置结构体状态,填充自己服务器ip与port    struct sockaddr_in local;    struct sockaddr_in client_addr;    local.sin_family = AF_INET;    local.sin_port = htons(atoi(argv[2]));    local.sin_addr.s_addr = inet_addr(argv[1]);    //绑定,监听套结字与自己服务器ip绑定    if( bind(listen_fd,(struct sockaddr*)&local,sizeof(local) )<0 )    {        perror(" fail  of  bind\n");        return -1;    }    //设置监听模式,创建队列,把用户信息填充到队列     listen (listen_fd,10);    printf("Listen--------\n");    //从队列获得客户端的请求信息,获得连接套结字    while(1)    {        printf("进入while\n");        socklen_t len = sizeof(client_addr);        //共享套结字(一个套结字从中拿数据,一个套结字从中拿数据往中写数据)        int connect_fd =accept(listen_fd,(struct sockaddr*)&client_addr,&len);//拿到监听者信息,建立链接        if(connect_fd<0)        {            perror("fail  to   accept\n");            return -1;        }        printf("######################################\n");        printf("recv from  ip  :  %s\n",inet_ntoa(client_addr.sin_addr));//客户端地址        printf("recv from  port  :  %d\n",ntohs(client_addr.sin_port));//客户端端口        printf("######################################\n");    char buf[1024] = {0};    int s =0;        memset(buf,0,sizeof(buf));        s =recv(connect_fd,buf,sizeof(buf),0);//从已连接的套结字中获取数据        printf("client--to--server:%s\n",buf);        memset(buf,0,sizeof(buf));        fgets(buf,sizeof(buf),stdin);        buf[strlen(buf)-1] = '\0';        s= send(connect_fd,buf,sizeof(buf),0);        printf("server-- to--client:%s\n",buf);        if(strncmp(buf,"quit",4)==0)        {            printf("Bye\n");            break;        }    close(connect_fd);    }    close(listen_fd);    return 0;}client.c#include <stdio.h>#include <sys/types.h>#include  <sys/socket.h>#include <string.h>#include <stdlib.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>int main(int argc,char* argv[]){    if(argc != 3)    {        printf("Usage  : %s[local_ip] [local_port]\n",argv[0]);        return -1;    }    //创建socket ,获得客户端套结字)    int sockfd;    sockfd = socket(AF_INET,SOCK_STREAM,0);    if(sockfd<0)    {        perror("touch sockfd fail\n");        return -1;    }    //设置结构体状态,填充服务器ip与port    struct sockaddr_in local;    struct sockaddr_in client_addr;    local.sin_family = AF_INET;    local.sin_port = htons(atoi(argv[2]));//port端口    local.sin_addr.s_addr = inet_addr(argv[1]);//本地ip    //向服务器发起连接请求    if(connect(sockfd,(struct sockaddr*)&local,sizeof(local))<0)    {        perror("connect  is  failed \n");        return -1;    }    //发送消息    char buf[1024] = {0};    while(1)    {        bzero(buf,sizeof(buf));//把数组内容改为0        putchar('>');        fgets(buf,sizeof(buf),stdin);//从标准输入中读取内容        buf[strlen(buf)-1] = '\0';        int s = send(sockfd,buf,strlen(buf),0);//向客户端套结字发送内容        if(s<0)        {            perror("send   is   failed\n");            return -1;        }    if(strncmp(buf,"quit",4)==0)            break;        printf("Client--to--Server: %d  bytes : %s\n",s,buf);        int n=read(sockfd,buf,sizeof(buf));        if(n<0)        {            perror("read   is   failed\n");            return -1;        }        printf("server--to--client: %d  bytes : %s\n",n,buf);    }   close(sockfd);   return 0;}

高并发服务器:

为何需要多进程(或者多线程),为何需要并发?
来一点生动的描述吧!!!假如你开了一个火锅店,店里只有一个员工,那所谓的劳动(拉客,招呼客人,做饭,打扫)都是他一个人干,还不得累死在店里,这时候,就需要很多个人来共同完成,所以就有了多进程(线程)概念。

并发性:就是你拥有了同一时间执行多条任务的能力,就比如在你招呼客人的时候,同时你也把外卖送到别人手中(分身能力 我的天!!),就会有更强大的能力,提供更多的服务。

多进程并发服务器:

使用多进程并发服务器考虑以下几点:
1、父进程最大文件描述符个数(父进程中需要close()掉accept返回的新的文件描述符)
2、系统内创建的进程的个数(与内存大小有关)
3、进程创建太多时,是否影响整体服务性能(进程调度均衡器)

fork后,子进程会复制父进程的task_struct结构,并为子进程的堆栈分配物理页。理论上来说,子进程应该完整地复制父进程的堆,栈以及数据空间,但是2者共享正文段。
关于写时复制:由于一般 fork后面都接着exec,所以,现在的 fork都在用写时复制的技术,顾名思意,就是,数据段,堆,栈,一开始并不复制,由父,子进程共享,并将这些内存设置为只读。直到父,子进程一方尝试写这些区域,则内核才为需要修改的那片内存拷贝副本。这样做可以提高 fork的效率。

//  多进程#include <unistd.h>  #include <sys/types.h>   #include <stdio.h>  void print_exit()  {         printf("the exit pid:%d/n",getpid() );  }  main ()   {      pid_t pid;      atexit( print_exit );      //注册该进程退出时的回调函数        pid=fork();           if (pid < 0)                   printf("error in fork!");           else if (pid == 0)                   printf("i am the child process, my process id is %d/n",getpid());           else           {                 printf("i am the parent process, my process id is %d/n",getpid());                 sleep(2);                wait();  //等待子进程退出        }  }  #############################connfd= Accept();pid = fork();if(pid ==0){   close(listenfd);   while (1)   close(connfd);}else if(pid>0){  close(connfd);}else perror("fork")close(listenfd);

多线程并发服务器:
使用多线程并发服务器考虑以下几点:
1、调整进程内文件描述符的上限
2、线程内容共享,需要考虑线程同步问题
3、服务于客户端的线程退出时,退出处理。(退出值,分离态)
4、系统负载,导致线程数量增加,其他线程不能及时得到CPU处理(均衡器)

server.hstruct s_info{    struct sockaddr_in _local;    int _connfd;}void* do_work(void* arg){    struct s_info* ts = (struct s_info*)arg;//一个线程的指针    pthread_detach(pthread_self())//设置线程创建属性,分离态。    while1)    {         s = read(ts->_connfd,buf,sizeof(buf));         ........         write()     }}#########################################main.c  pthread_t tid;  struct s_info ts[256];  .....  connfd = accept(listenfd,(struct sockaddr*)&local,&(sizeof(local)));  ts[i]._local =local;  ts[i]._connfd = connfd;  pthread_create(&tid,null,do_work,(void*)&ts[i]);  //达到线程最大时,create()函数出错处理,增加服务器稳定性  i++;pthread_detach用于分离可结合线程tid。线程能够通过以pthread_self()为参数的pthread_detach调用来分离它们自己。    如果一个可结合线程结束运行但没有被join,则它的状态类似于进程中的Zombie Process,即还有一部分资源没有被回收,所以创建线程者应该调用pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源。    由于调用pthread_join后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此。例如,在Web服务器中当主线程为每个新来的连接请求创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的连接请求),这时可以在子线程中加入代码    pthread_detach(pthread_self())

线程就是把一个进程分为很多片,每一片都可以是一个独立的流程。这已经明显不同于多进程了,进程是一个拷贝的流程,而线程只是把一条河流截成很多条小溪。它没有拷贝这些额外的开销,但是仅仅是现存的一条河流,就被多线程技术几乎无开销地转成很多条小流程,它的伟大就在于它少之又少的系统开销。

四.比较以及注意事项

1.多进程和多线程的区别
前者开销大,后者开销较小,这就是最基本的区别。
2.多进程会把同一个变量在每个进程中拷贝一份,互相不影响。
3.多线程每个变量都是由所有线程共享,因此,多线程最大的危险在于多个线程同时修改一个变量,容易把内容改乱了,这就是竞争

2.线程函数的可重入性:

线程安全:

多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染,线程安全都是由全局变量或静态变量引起的

可重入:概念基本没有比较正式的完整解释,但是它比线程安全要求更严格。根据经验,所谓“重入”,常见的情况是,程序执行到某个函数foo()时,收到信号,于是暂停目前正在执行的函数,转到信号处理函数,而这个信号处理函数的执行过程中,又恰恰也会进入到刚刚执行的函数foo(),这样便发生了所谓的重入。此时如果foo()能够正确的运行,而且处理完成后,之前暂停的foo()也能够正确运行,则说明它是可重入的。

判断线程可能存在线程安全的条件:
1、是不是多线程运行条件下?
2、多个线程是不是共享一块资源,并对其进行读写操作。

线程安全的条件:
要确保函数线程安全,主要需要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存器。因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访问时,如果要保证线程安全,
1、设置局部变量(封装一个属性,让每个线程私有一份)
2、必须通过加锁的方式。

要确保函数可重入,需满足一下几个条件:
1、不在函数内部使用静态或全局数据
2、不返回静态或全局数据,所有数据都由函数的调用者提供。
3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
4、不调用不可重入函数。

3.关于IPC(进程间通信)

由于多进程要并发协调工作,进程间的同步,通信是在所难免的。

linux下进程间通信的几种主要手段简介:

管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);

报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

或许你会有疑问,那多线程间要通信,应该怎么做?前面已经说了,多数的多线程都是在同一个进程下的,它们共享该进程的全局变量,我们可以通过全局变量来实现线程间通信。如果是不同的进程下的2个线程间通信,直接参考进程间通信。

4.关于线程的堆栈

生成子线程后,它会获取一部分该进程的堆栈空间,作为其名义上的独立的私有空间。(为何是名义上的呢?)由于,这些线程属于同一个进程,其他线程只要获取了你私有堆栈上某些数据的指针,其他线程便可以自由访问你的名义上的私有空间上的数据变量。(注:而多进程是不可以的,因为不同的进程,相同的虚拟地址,基本不可能映射到相同的物理地址)

在线程里面fork会出现什么情况?
fork所做的事:将父进程的内存数据原封不动的拷贝一份给子进程
子进程在单线程条件下生成
在内存区域,静态变量的区域会被拷下来,父进程中即使有多个线程,他们也不会被继承到子进程中,
所以死锁的原因就很明确了::
当一个线程去执行一个函数时,给函数静态变量加上了锁,
而fork子进程时,把内存数据拷贝给了子进程,但是并没有把线程拷贝过去(那个函数中的静态变量还处于加锁状态)
子进程再次调用函数时,发现函数静态变量已经加锁,就会一直等待,直到有进程释放它(可实际没有人拥有锁)
线程执行完成了,会把自己的释放,可是,子进程的那块是一份拷贝,并与线程的那块无关,所以就一直得不到释放,而造成死锁。

多进程与多线程的好坏,什么场景该如何选择?

这里写图片描述
1)需要频繁创建销毁的优先用线程
原因请看上面的对比。
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的
2)需要进行大量计算的优先使用线程
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
这种原则最常见的是图像处理、算法处理。
3)强相关的处理用线程,弱相关的处理用进程
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。
4)可能要扩展到多机分布的用进程,多核分布的用线程
原因请看上面对比。
5)都满足需求的情况下,用你最熟悉、最拿手的方式
至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,
实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。

原创粉丝点击