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.\n",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],跳过......\n",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

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

posted @ 2019-12-15 22:34  水鬼子  阅读(616)  评论(0编辑  收藏  举报