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
---------------------------------------------------------------------------------------------