zoukankan      html  css  js  c++  java
  • socket编程 之 TCP用法研究

    服务端:
    //创建多个socket,分别绑定到一个端口号上,然后哦监听着,同时添加到epoll模型中,等待有事件触发后分别accept
    int main(int argc,const char* argv[]){//1.创建一个epoll实例,里面一共可以监听1000个udp socketint epfd=epoll_create(MAXEPOLLSIZE);for(int i=port; i<port+10; i++){ fd= socket(PF_INET, SOCK_STREAM, 0);

         fcntl(fd, F_SETFL, fcntl(fd, F_GETFD, 0)|O_NONBLOCK); //使用epoll用来accept的话,1)阻塞模式;2)非阻塞的
    //3.将socket封装成事件添加到epoll模型中 struct epoll_event ev;ev.data.fd=fd; ev.events=EPOLLIN|EPOLLET; //边界模式 int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev); //注册epoll事件 res=tcpBind(fd,ip,i); res=listen(fd,1024); } while(1){ //4.开始等待epoll实例中的socket们发生事件,发生的所有事件都会记录在events中 nfds=epoll_wait(epfd,events,100,0); if(nfds <= 0)continue;//5.既然已经有事件发生了,有nfds个事件发生,那么处理之:是哪个socket发生的,以及发生的是什么事件。 for(int i=0;i<nfds;++i) { int accept_fd=accept(events[i].data.fd,(struct sockaddr*)&client_addr,&len); printf("[Success][Accept]by<fd:%d>,Client:%s:%d, new fd:%d. ",events[i].data.fd,inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port),accept_fd);

           //方式一:阻塞是接收报文 recv_cnt = recv(accept_fd, recvbuf, 200, 0);

            //方式二:非阻塞式接收报文之使用while循环
            //如果过不使用select/poll/epoll模型,则必须搞一个循环不停的accept。否则是没办法工作的,因为你不阻塞在这里等着别人连,那么就要求电光火石之间三次握手建立,而这个是不可能的,所以应该如下操作
            while(accept_fd<0 && errno==EAGAIN){
              accept_fd=accept(listen_fd,(struct sockaddr*)&client_addr,&len);
            }

            //方式三:非阻塞式接收报文之交给epoll处理
            if(accept_fd<0){   //如果接收连接失败了,就有可能不是连接请求事件,就有可能是报文发送事件,于是尝试接收数据报文
              recv_cnt = recv(events[i].data.fd, recvbuf, 200, 0); 
              continue;
            }

            //如果连接建立成功了,就将新fd(接收socket)设置成非阻塞模式,然后添加到epoll模型中,用来接收之后可能进来的数据        
            fcntl(accept_fd, F_SETFL, oldflag|O_NONBLOCK);

            struct epoll_event ev;
            ev.data.fd=accept_fd;
            ev.events=EPOLLIN; //缺省:水平模式; 边界模式:EPOLLET
            int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,accept_fd,&ev);

            //非阻塞式等待接收报文
            recv_cnt = recv(accept_fd, recvbuf, 200, 0); 

            }
        }
    }
    客户端:
    //一个线程:创建一个socket,向服务端发送连接请求connect(),连接建立之后发送数据send(),发送后接收数据,每一步都是阻塞模式,即一步一步走下去
    void *handerFunc(void *argc){ int fd= socket(PF_INET, SOCK_STREAM, 0); res=connect(fd,(struct sockaddr*)&server, len); if(res>=0){ getsockname(fd, (struct sockaddr*)&local_addr, &len);while(count<0 || --count>0){ res=send(fd, Data, PACKAGE_LEN, 0); if(res > 0 ){ res = recv(fd,recvBuf,PACKAGE_LEN,0); if(res<=0){ printf(" [Recv][Error:%d][thread:%d],跳过...... ",errno,thread_no,fd); continue;} } } } } //启用多线程分别向服务端的一个端口发送连接请求 int main(int argc,const char* argv[]){ pthread_t tid[NUM_THREAD]; unsigned short int thread[NUM_THREAD]; for(i = 0; i < NUM_THREAD; i++){ thread[i]=i;} for(i = 0; i < NUM_THREAD; i++){ pthread_create(&tid[i],NULL,handerFunc,(void *)&thread[i]); } }

    结果1:

    服务端:

    >>>>>have 3 events happened.
    [Success][Accept]by<fd:6>,Client:192.168.1.158:57792, new fd:9.
    [Success][Recv:200] by [fd:9],Content:####HELLO ....
    [Success][Accept]by<fd:4>,Client:192.168.1.158:37138, new fd:10.
    [Success][Recv:200] by [fd:10],Content:####HELLO ...
    [Success][Accept]by<fd:5>,Client:192.168.1.158:49102, new fd:11. ----卡在客户端的thread1的49102端口对应的连接上,并且因为没有接收到数据而阻塞住

    客户端进程退出后:
    [Error:0][Recv] by [fd:11].  ----对方tcp连接断开,于是我结束卡死的状态
    >>>>>have 2 events happened.  
    [Success][Accept]by<fd:8>,Client:192.168.1.158:34748, new fd:12.
    [Success][Recv:200] by [fd:12],Content:####HELLO ...
    [Success][Accept]by<fd:7>,Client:192.168.1.158:35514, new fd:13.
    [Error:0][Recv] by [fd:13].

    客户端:

    [Connect][Success][thread:2]Server:[192.168.1.195:30002] by fd[7] and addr[192.168.1.158:57792].
    [Send][Success][thread:2]by [fd:7]成功.
    [Connect][Success][thread:0]Server:[192.168.1.195:30000] by fd[5] and addr[192.168.1.158:37138].
    [Send][Success][thread:0]by [fd:5]成功.
    [Connect][Success][thread:1]Server:[192.168.1.195:30001] by fd[6] and addr[192.168.1.158:49102].-----连接是建立成功了,但是并没有发送数据出去
    [Connect][Success][thread:3]Server:[192.168.1.195:30003] by fd[8] and addr[192.168.1.158:35514].----- 连接是建立成功了,但是并没有发送数据出去
    [Connect][Success][thread:4]Server:[192.168.1.195:30004] by fd[9] and addr[192.168.1.158:34748].------连接建立成功,并且发送成功
    [Send][Success][thread:4]by [fd:9]成功.

    [root@localhost socket_test]# netstat -auntp|grep 35514
    tcp 0 0 192.168.1.158:35514 192.168.1.195:30003 ESTABLISHED 17818/tcp_client

    然后,退出客户端进程

    解析:

    1)为什么服务端显示只有3个连接,而客户端显示连接全部建立成功

    答:需要详细说说关于tcp的三次握手

          基本上三次握手的行为是在服务端处于listen状态的时候,客户端调用connect完成的,连接成功后双方的端口处于ESTABLISHED状态;

      但是需要注意的是,连接的两端是:客户端就是发起时使用socket及对应的端口号,而服务端在连接成功后会新生成一个socket(with port),处于ESTABLISHED指的是这一对端口号

      而至于服务端accept(),其实是从ESTABLISHED状态的端口号/socket中取出那个新生成的socket,即连接是否建立成功并不是通过accept()完成的,accept()只不过是去"取出来"

      所以,在上面的例子中,尽管服务端获取到3个连接,但是客户端显示所有5个线程的连接全部建立

    2)为什么会卡住

    答:首先,服务端是串行处理,接收到第3个连接后等待接收数据,但是客户端没有发送数据,

           然后,客户端为什么没有发送,因为count是个全局变量,哈哈哈,是一个失误啦...不过刚好因为这个失误梳理了下tcp连接建立的过程

    3)为什么客户端进程结束后,服务端进程得以继续

    答:因为卡住的那个是因为在等待接收数据,连接断开后也不等待了直接结束,于是程序继续,进入第二轮取epoll中获取,把剩下两个tcp连接获取出来,

           其中,来自客户端端口号为34748的连接因为有过发送,所以连接还没有关闭,所以从连接的buffer中可以把数据取出来。

    剩下的那个也没数据连接也断开了,所以直接返回

    另外需要说明一下accept函数

      如果不对socket进行额外设置的话,accept()本身是一个阻塞式的函数,即一直等着有客户端向我的listen socket发起连接(tcp三次握手),但是有了epoll后,即使不额外设置socket也没关系,因为我先不去调用accept,只有真的有时间发生了,此时再调用accept则一调一个准

    2,在服务端将创建的监听socket设置成非阻塞模式:

    结果1:如果accept的时候没有使用while循化,则会直接报错11,如下:
      Bind 192.168.1.158:30000 Success, the flag of this fd:802. nonflag:800
      [Error:11][Accept]by<fd:-1>,so return.

    结果2:使用while循环,成功接收连接后,对于新生成的accept fd,其flag并没有继承原监听socket的flag,因此需要显式设置一下:
      [Success][Accept]by<fd:7>,Client:192.168.1.246:63828, new fd:15 and new flag of new fd:2,so set O_NONBLOCK.

    结果3:在epoll中accept连接后,将新生成的fd再添加到epoll中,不过使用的是水平模式,于是一方面可以顺利接收客户端的连接,连接失败了则尝试接收报文,在这里报文不全部接收,而是先接收200字节

                处理一轮的epoll事件后,因为是水平模式,所以如果socket的buffer中还有数据则会再次触发事件,继续接收

    >>>>>have 4 events happened. ----有4个事件发生,分别是什么呢
    [Success][Accept]by<fd:6>,Client:192.168.1.246:54732, new fd:11.  ---事件1:接收client的连接1,同时也接收到了数据过来(200字节),同时新封装一个event扔给epoll
    [Success][Recv:200] by [fd:11],Content:####HELLO , ...            ---同时还接收了事件1中连接的
     ---事件2: 同事件1...
    [Success][Accept]by<fd:8>,Client:192.168.1.246:33620, new fd:13.  ---事件3:接收client的连接3,没有接收到数据过来,于是新封装一个event扔给epoll
    ---事件4:同事件3...
    >>>>>have 3 events happened. ----有4个事件发生,分别是什么呢  
    ---事件5:同事件1...
    
    [Error:22][Accept]by<fd:11>,so continue to check if some data come onin.  ---事件6:继续事件1中的接收,事件1中没有接收完,因为是水平模式,所以仍然算是一次事件触发,所以继续接收(200字节,累计400字节)
    [Success][Recv:200] by [fd:11] from epoll,Content:d job.3....
    --事件7:继续事件2的接收,同事件6
    >>>>>have 3 events happened.   ----有4个事件发生,分别是什么呢  
    [Error:22][Accept]by<fd:11>,so continue to check if some data come onin.  --事件8:接收继续事件1中的最后的内容(112个字节,累计512字节)
    [Success][Recv:112] by [fd:11] from epoll,Content:...
    --事件9:接收最后的内容112个字节
    ...

     结果4:在epoll中accept连接后,将新生成的fd再添加到epoll中,不过使用的是边界模式

    >>>>>have 3 events happened.
    [Success][Accept]by<fd:10>,Client:192.168.1.246:23332, new fd:11 and new flag of new fd:2,so set O_NONBLOCK.  ---事件1: 接收连接
    [Error:11][Recv] by [fd:11].
     ---事件2,3: 同事件1,接收连接
    >>>>>have 1 events happened.
    [Error:22][Accept]by<fd:11>,so continue to check if some data come onin.  ---事件4,事件1中的连接第一次有数据来了,于是接收(200字节)
    [Success][Recv:200] by [fd:11] from epoll,Content:####HELLO ,..
    ...
    [Error:22][Accept]by<fd:15>,so continue to check if some data come onin.  ---事件8: 事件7中的连接第一次有数据来了,于是接收(200字节)
    [Success][Recv:200] by [fd:15] from epoll,Content:####HELLO ,...
    .......之后没再有事件发生......
    
    # netstat -auntp|grep 3000
    tcp      312      0 192.168.1.158:30002     192.168.1.246:33648     ESTABLISHED 27632/tcp_server     ---只接收200字节,剩了312字节还再buffer中  
    tcp        0      0 192.168.1.158:30003     192.168.1.246:44050     ESTABLISHED 27632/tcp_server    
    tcp      312      0 192.168.1.158:30001     192.168.1.246:63848     ESTABLISHED 27632/tcp_server    
    tcp      312      0 192.168.1.158:30004     192.168.1.246:23332     ESTABLISHED 27632/tcp_server    
    tcp        0      0 192.168.1.158:30000     192.168.1.246:54766     ESTABLISHED 27632/tcp_server

     --------------------------

    F_GETFD和 F_GETFL

     F_SETFD/F_GETFD:设置/读取文件描述符的标志。 

     F_SETFL/ F_GETFL:设置/读取文件描述符状态标志。

    关于文件描述符的一些知识点:

    1.内核会维护3个数据表

        1)进程级的文件描述符表,用来记录文件的描述符(open file description),一个条目代表一个文件相关信息,包括

            (1)文件句柄,一个指针

            (2)控制文件描述符的一组标志(目前只有close-on-exec标志,设置了该标志后,这个进程如果结束了,则关闭这个进程下的文件句柄)

        2)系统级的打开文件表(open file description table),表中每个条目称为open file description

           注:系统级的和进程级的表中存放的条目不是一样的,尽管其代表的最终目标是一样的,所以我所参考的著作中将系统级的表叫做:open file table,表条目叫open file handle。

             (1)当前文件偏移量,即read(),write()时会更新这个偏移量,使用lseek可以直接修改这个偏移量

             (2)打开的文件的状态标志

             (3)文件访问模式

      (4)与信号驱动I/O相关的设置

      (5)对文件i-node对象的引用

        3)系统级(文件系统)的i-node表,每一个条目代表一个文件:一个条目主要的信息包括:文件类型,一个指向持有锁列表的指针,文件的各种属性等..

      其对应关系可以用如下图所示:

    即:1个进程可以有多个fd指向同一个打开的文件描述符; 多个进程的fd也可以指向同一个打开的文件描述符,比如父进程fork出一个子进程。此时所有fd共享文件偏移量

           多个打开文件描述符可以指向同一个i-node

           

    综上:

    F_SETFL/ F_GETFL:是针对系统级的打开文件描述符的装填的设置

     F_SETFD/F_GETFD:是进程下的fd独有的,对某个fd设置后,不会影响同进程或不同进程中其他fd。目前这个参数只用来设置FD_CLOEXEC

     用法注意:

    1)错误的用法:fcntl(socket,F_SETFL,fcntl(socket,F_GETFD)|O_NONBLOCK);

       正确的用法:fcntl(socket,F_SETFL,fcntl(socket,F_GETFL)|O_NONBLOCK);

       使用错误的用法,非阻塞是设置成功了,那是因为fcntl(socket,F_GETFD)得到的值时0,相当于直接设置O_NONBLOCK

    ---------------------------------------------------------------------------------------------

  • 相关阅读:
    Android 音视频同步机制
    FFmpeg命令行工具学习(五):FFmpeg 调整音视频播放速度
    Android框架式编程之RxJava
    Android Gradle 学习笔记(一):Gradle 入门
    FFmpeg开发实战(六):使用 FFmpeg 将YUV数据编码为视频文件
    SDL 开发实战(七): SDL 多线程与锁机制
    JNI实战(四):C 调用 Java
    JNI实战(三):JNI 数据类型映射
    JNI实战(二):Java 调用 C
    JNI实战(一):JNI HelloWorld
  • 原文地址:https://www.cnblogs.com/shuiguizi/p/12030317.html
Copyright © 2011-2022 走看看