(一)基本socket api介绍—-TCP协议
socket
创建一个通信的端点。返回的是一个文件描述符fd:对于客户端来说,就是通过fd来与服务器来发起通信的,对于服务器来说,这个就是一个监听套接字。
bind
把socket创建的套接字绑定到指定的ip和port,因为socket系统调用就是告诉操作系统,我要一个通信啦,你要给我准备好来,这样,os就给我们创建了一个基本的数据结构,但是里面还没有填充数据,bind函数就是给数据结构填充数据的。
listen
把套接字变成监听套接字,对应的TCP状态为状态LISTEN。监听套接字只能用来监听用,也即只能做accept参数。
accept
套接字默认是阻塞的,当没有连接到来时,服务器一直阻塞在该调用上。当客户一旦调用connect函数,服务端也监听了套接字,那么内核给我们做了连个队列,一个是正在做三次握手的队列,一个是做好三次握手的队列。accept函数就是在做好三次握手队列中拿到一个连接,并且返回一个连接描述符connfd,这样我们就可以通过connfd描述符来与客户端通信了。
如果要做多并发服务器,那么只要像下面的模板一样
这个是进程的例子:
for()
{
connfd = accept();
pid = fork();
if(pid == 0)
{
close(listenfd);//子进程不需要监听套接字
while(1)
{
read(connfd ,...);
dothing();
write(connfd ,...);
}
close(connfd);//父进程不需要连接套接字
exit(0);//子进程不参与下一次的fork。。。note
}
close(connfd);
}
这个是线程的例子:
while(1)
{
connfd = accept();
//方法一:给线程传递描述符,也即连接套接字,;
//方法二:malloc一个,再在线程函数中free,方法一可行尽量使用第一种,这样就不需要使用malloc/free,这样的非线程安全函数
pthread_create(&tid ,NULL , th_fun , (void*)connfd);
}
----------------------------------------
void * th_fun(void * arg)
{
int connfd = (int)arg;
while(1)
{
read(connfd ,...);
dothing();
write(connfd ,...);
}
}
connect
调用该函数可以向客户端发起三部握手,默认是阻塞的,当正确返回时,就可以通过sockfd通信了。
close /shutdown
1、close的机制是描述符的引用技术。shutdown是直接发起关闭请求的。
2、close关闭的读写两端,shutdown可以有选择的关闭读写端
3、close,shutdown(r)都会激发tcp/ip的FIN分节的发送,对端读出就是0,说明关闭了。一定要处理的。
4、注意管道破裂,产生SIGPIPE信号。
setsocketopt
man 7 socket ip tcp可以找到都有那些选项
设置套接字选项。主要有SOL_SOCK,TCP_ , IP_,其中主要介绍几个用的上的。
//SO_ERROR
int optval;//其实就是一个开关
getsockopt( sockfd, SOL_SOCK, SO_ERROR, &optval, sizeof(int);
//对于设置非阻塞套接字的connect函数和accept函数,判断是否发生了错误
-----------------------------------------
//SO_LINGER
//出现的原因是,当我们还有数据没有发完的时候,我们调用了close函数,如果设置了这个选项,那么就是告诉内核,你//等一等吧//(可以设置的等待时间),在关闭套接字。
struct linger lin;
lin.l_onoff = 1;//为1表示我们要打开这个选项, 0表示我们关闭了这个选项
lin.l_linger = 5;//延迟的时间,现在是5秒
setsockopt( sockfd, SOL_SOCK, SO_LINGER, &lin, sizeof(lin);
------------------------------------------------------------------------
//SO_REUSEADDR
//地址复用选项,出现的原因是,当服务器处于TIME_WAIT状态是,需要等待2MSL时间,一般都是超过一分钟,服务器一般都是绑定ip//和端口的,不设置的话,那么服务器就启动不起来。设置这个状态给最后的一个ack可以到达对端;允许重复的分节在网络中消失。
int optval = 1;//也是一个开关选项
setsockopt(sockfd,SOL_SOCK , SO_REUSEADDR ,&optval ,sizeof(int));
---------------------------------------------------------------------------
//SO_RCVBUF and SO_SNDBUF
//SO_RCVLOWAT and SO_SNDLOWAT
这两个一般是不设置,因为close调用会发送一个
(二)使用selsect函数设置connect和accept函数的超时
connect_timeout()
int connect_timeout(int sockfd , struct sockaddr * addr , int len , long timeout)
{
int ret = 0;
ret = set_nonblock(sockfd , 0);//设置套接字的非阻塞模式
if(ret == -1)
{
printf("fun connect_timeout() , set_nonblock() err\n");
return ret;
}
ret = connect(sockfd ,addr , len );//直接连接一下,看看能不能立即返回,能最好,不能那么就使用下面的select超时优化
if(ret != -1)
{
return ret;
}
if(ret == -1)
{
if(errno != EINPROGRESS)
{
//perror("fun connect_timeout() connect() err");
return ret;
}
}
ret = set_nonblock(sockfd , 1);
if(ret == -1)
{
printf("fun connect_timeout() , set_nonblock() err\n");
return ret;
}
fd_set rset ;
FD_ZERO(&rset);
FD_SET(sockfd , &rset);
struct timeval tv;
tv.tv_sec = timeout;
tv.tv_usec = 0;
ret = select(sockfd+1 , &rset , NULL ,NULL , &tv);
if(ret == -1)
{
//perror("fun connect_timeout() select() err");
return ret;
}
if(ret == 0)
{
ret = CLIENT_TIMEOUT;
ret = -1;
return ret;
}
return 0;
}
accept_timeout
int accept_timeout(int sockfd , int timeout)
{
int ret = set_nonblock() (sockfd , 0);
if(ret == -1)
{
printf("fun accept_timeout() set_nonblock() err\n");
return ret;
}
int fd = accept(sockfd , NULL , NULL);
if(fd == -1)
{
// EAGAIN or EWOULDBLOCK
if(errno != EAGAIN && errno != EWOULDBLOCK)
{
perror("fun accept_timeout() accept() err");
return fd;
}
}
if(fd >= 0)
{
return fd;
}
struct timeval tv;
tv.tv_sec = timeout;
tv.tv_nsec = 0;
fd_set rset;
FD_ZERO(&rset);
FD_SET(sockfd ,&rset );
begin:
ret = select(sockfd +1 , &rset , NULL , NULL , &tv);
if(ret == -1)
{
if(errno == EINTR)
{
goto begin;
}
perror("fun accept_timeout() select() err");
return ret;
}
if(ret == 0)
{
errno = CLIENT_TIMEOUT;
printf("fun accept_timeout() accept twe select() timeout");
return -1;
}
fd = accept(sockfd , NULL , NULL);
if(fd == -1)
{
perror("fun accept_timeout() accept twe err ");
return fd;
}
return fd;
}
(三)TCP/IP状态转化图
注意一下几点:
1、监听的时候内核给我们做了两个队列;
2、先执行close的一方先推进到time_wait状态;
3、如果对方不给close,那么本端就一直处于半连接状态,也即FIN_WAIT2状态。
4、为什么三次握手,四次断开?
心跳函数
讲的就是带外数据或者说紧急指针的概念。以及信号的处理。
对于发送带外数据:
send(fd , “a” , 1 , MSG_OOB);
在程序中写上这句话,有两个动作。动作1:给对端发送一个TCP首部带有URG标记的分节给对端, 分节中包含紧急数据的指针即位置,这个动作不管缓冲区,通告窗口的大小,直接就发送过去了。
动作2:把send的数据“a”放在发送队列中进行排队发送。
对于接受带外数据:
recv(fd , &recvbuf , 1 , MSG_OOB);
SO_OOBINLINE在线留存选项的设置与否。当没有设置时,带外数据是放在一个独立的单字节带外缓冲区中。
注意,如果你在接受的时候,发送紧急数据过快,你接受紧急数据过慢,可能会被覆盖。
当设置的时候,那么带外数据是放在套接字的接受缓冲区中。可以通过sockatmark()函数来确认是否含有带外数据。
使用select机制读取带外数据
带外数据只能读取一遍,读完之后,tcp会清空单字节的带外缓冲区,如果select继续检测异常条件,那么就会一直返回异常。
while(1)
{
FD_SET(fd , &rset);
if(justread == 0)
{
FD_SET(FD , &xset)
}
select(fd+1 , &rset , NULL , &xset , 0);
if(FD_ISSET(fd , &rset))
{
read();
....
}
if(FD_SET(fd , &xset))
{
recv(...,MSG_OOB);
justread = 1;
FD_CLR(fd , &xset);
}
justread = 0;
}
客户/服务器心博函数
机制:当tcp收到带外数据时,内核给进程发送一个SIGURG信号,利用这一机制,就可以写出心博函数了。
客户端的信号处理程序
sig_alrm()
{
if(++cnt>5)exit(0);
send(MSG_OOB);
alarm(1);
}
sig_urg()
{
recv(MSG_OOB);
cnt = 0;
}
服务器端
sig_urg()
{
recv(MSG_OOB);
send(MSG_OOB);
cnt = 0;
}
sig_alrm()
{
if(++cnt>5)exit(0);
alarm(1);
}