TCP基本套接字函数

来源:互联网 发布:淘宝怎么查看排名 编辑:程序博客网 时间:2024/06/05 02:49

基本的TCP客户/服务器程序的套接字函数:
TCP服务器:
socket()
bind()
listen()
accept()

read()
write()
close()

TCP客户:
socket()
connect()
write()
read()
close()

socket函数
为了执行网络i/o,一个进程必须做的第一件事就是调用socket函数,指定期望的通信协议

#include<sys/socket.h>int socket(int family,int type,int protocol);

family指明协议族,常用的有:(还有可能碰到以PF_开头的)
AF_INET ipv4
AF_INET6 Ipv6
AF_LOCAL Unix域协议(有可能碰到以AF_UNIX代替)
AF_ROUTE 路由套接字
AF_KEY 密钥套接字

type指明套接字类型,常用的有:
SOCK_STREAM 字节流套接字
SOCK_DGRAM 数据报套接字
SOCK_SEQPACKET 有序分组套接字
SOCK_RAM 原始套接字

protocol是某个协议类型常值,默认为0,以选择所给定family和type组合的系统默认值。
IPPROTO_TCP TCP传输
IPPROTO_UDP UDP传输
IPPROTO_SCTP SCTP传输

socket函数在成功时返回一个小的非负整数值,它与文件描述符类似,我们把它称为套接字描述符sockfd。

connect函数
connect函数用于TCP客户建议与TCP服务器的连接

#include<sys/socket.h>int connect(int sockfd,const struct sockaddr* servaddr,socklen_t addrlen);//成功返回0,出错返回-1

第二个第三个参数分别是一个指向套接字地址结构的指针和该结构的大小。
如果是TCP套接字,调用connect函数将激发TCP的三路握手过程,而且仅在连接建立成功或出错时才返回。

connect函数出错的返回的几种情况
1 若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误。

2 若对客户的SYN的响应是RST,则表明该服务器主机在我们指定的端口上没有进程在等待与之连接,客户一接收RST就马上返回ECONNREFUSED错误。
RST是TCP在发生错误时发送的一种TCP分节。
产生RST的三个条件是:
1 目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器。
2 TCP想取消一个已有连接
3 TCP接收到一个根本不存在的连接上的分节。

3若客户发出的SYN在中间的某个路由器上引发了ICMP错误,那么会把保存的消息作为EHOSTUNREACH或ENETUNREACH错误返回给进程。

connect函数导致当前套接字从CLOSED状态转移到SYN_SENT状态,若成功再转移到ESTABLISHED状态。
若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。
当循环调用函数connect为给定主机尝试各个ip地址直到有一个成功时,在每次connect失败后,都必须close当前的套接字描述符并重新调用socket。

bind函数
bind函数通常把一个本地协议地址赋予一个套接字。
协议地址是32位的IPv4地址或是128位的ipv6地址与16位的TCP或UDP端口号的组合。

#include<sys/socket.h>int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen);//若成功则为0,若出错则为-1。

对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,也可以都不指定。
服务器在启动时捆绑它们的众所周知端口,如果一个TCP客户或服务器未调用bind捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时端口。对于TCP客户一般使用临时端口,但对于服务器一般使用众所周知的端口。(对于RPC服务器一般使用临时端口)
进程可以把一个特定的IP地址捆绑到它的套接字上,对于TCP客户,这就为该套接字发送的IP数据报指派了IP地址,对于TCP服务器,这限定了该套接字只接收那些目的地为这个IP地址的客户连接。
TCP客户通常不把IP地址捆绑到它的套接字上,如果TCP服务器没有把IP地址捆绑到它的套接字上,内核就把客户发送的SYN的目的IP地址作为服务器的源IP地址。
如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。
对于IPv4来说,通配地址由常值INADDR_ANY来指定,值一般为0。

struct sockaddr_in servaddr;servaddr.sin_addr.s_addr=hton1(INADDR_ANY);

对于IPv6

struct sockaddr_in6 serv;serv.sin6_addr=in6addr_any;

头文件

#include<sys/socket.h>int listen(int sockfd,int backlog);

listen函数只能由TCP服务器调用,listen函数主要做两件事情
1 当socket函数创建一个套接字时,它就被作为一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换为一个被动套接字,指示内核应接受指向该套接字的连接请求。
2 第二个参数规定了内核应该为相应套接字排队的最大连接个数。

这个函数应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用。
内核为任何一个给定的监听套接字维护两个队列:
1 未完成连接队列 已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
2 已完成连接队列 每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED

每当客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三路握手的第二个分节:服务器的SYN响应,其中捎带对客户SYN的ACK。这一项一直保留在未完成连接队列中,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或该项超时为止。
如果三路握手正常,该项就从未完成队列移到已完成队列的队尾,调用accept函数时,已完成连接队列中的队头项将返回给进程。

listen函数的backlog参数被规定为这两个队列总和的最大值。那么这个值的大小为多大呢?
HTTP服务器一般要处理好几百万个连接,所以必须指定一个足够大的值,如果这个指定值在源代码中是一个常值,那么增长其大小需要重新编译服务器程序。另一个方法是设定一个默认值,不过允许通过命令行选项或环境变量改写该默认值
我们定义一个包裹函数来解决这个问题,定义一个LISTENQ可以重新定义。

void Listen(int fd,int backlog){char *ptr;if((ptr=getenv("LISTENQ"))!=NULL)//读取环境变量当前值的内容backlog=atoi(ptr);//把字符串转换为整形数的函数。if(listen(fd,backlog)<0)err_sys("listen error");

accept函数
accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。

#include"unp.h"#include<time.h>int main(int argc,char**argv){    int listened,connfd;    socklen_t len;    struct sockaddr_in servaddr,cliaddr;    char buff[MAXLInE];    time_t ticks;    listened=Socket(AF_INET,SOCK_STREAM,0);    bzero(&servaddr,sizeof(servaddr));    servaddr.sin_family=AF_INET;    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);    servaddr.sin_port=htons(13);    Bind(listened,(SA*)&servaddr,sizeof(servaddr));    Listen(listened,LISTENQ);    for(;;)    {        len=sizeof(cliaddr);        connfd=Accept(listened,(SA*)&cliaddr,&len);        printf("connection from %s.port %d\n",inet_ntop(AF_INET,&cliaddr.sin_addr,buff,sizeof(buff))),ntohs(cliaddr.sin_port);        ticks=time(NULL);        snprintf(buff,sizeof(buff),"%.24s\r\n",ctime(&ticks));        Write(connfd,buff,strlen(buff));        Close(connfd);    }}

fork和exec函数
fork函数是unix中派生新进程的唯一方法,可以用它来编写并发服务器

#include<unistd.h>pid_t fork(void);

fork函数调用一次会返回两次,在父进程(调用进程)中返回一次,在子进程(派生进程)中返回一次。在父进程返回的是子进程的进程ID,在子进程中返回的是0(不返回父进程ID的原因在于每个子进程都只有一个父进程,可以通过getppid获得父进程id,但每个父进程可以有很多个子进程。)
父进程中调用fork之前打开的所有描述符在fork返回之后由子进程分享。所以,例如父进程调用accept之后调用fork。所接受的已连接套接字随后在父进程和子进程之间共享。通常情况下,子进程接着读写这个已连接套接字,父进程则关闭这个已连接套接字。

fork有两个典型的用法:
1 一个进程创建这个进程的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。
2 一个进程想要执行另一个程序。该进程首先调用fork创建一个自身的副本,然后其中一个副本调用exec()把自身替换成新的程序。

存放在硬盘上的可执行程序文件能够被unix执行的唯一方法是:由一个现有进程调用六个exec函数中的某一个。exec把当前进程映像替换成新的程序文件,而且该进程通常从main函数开始执行。进程ID并不改变。我们称调用exec的进程为调用进程,称新执行的进程为新程序。

这六个exec函数之间的区别是:待执行的程序文件是由文件名还是路径名指定;新程序的参数是一一列出还是由一个指针数组来引用;用进程的环境传递给新程序还是给新程序指定新的环境。
所需头文件

#include

//典型的并发服务器轮廓pid_t pid;int listenfd,connfd;listenfd=socket(...);Bind(listenfd,...);Listen(listenfd,LISTENQ);for(;;){connfd=Accept(listenfd,...);if((pid==Fork())==0){    Close(listenfd);//子进程关闭监听套接字,父进程继续保留监听套接字    doit(connfd);    Close(connfd);//处理完时,显式地关闭已连接的套接字,这点不是必要的因为有exit()    exit(0);}close(connfd);//关闭父进程的已连接套接字。父进程只用于监听,子进程用于处理。}

当一个连接建立时,accept返回,服务器接着调用fork,然后由子进程服务客户,父进程等待另一个连接。

对一个TCP套接字调用close会导致发送一个FIN,随后是正常的连接终止序列。但为什么在父进程中对connfd调用close没有终止它与客户的连接呢?,因为套接字描述符有一个引用计数,调用子进程时,计数+1。如果父进程对每个accept返回的以连接的套接字都不调用closed,那么将耗尽可用描述符。还有就是妨碍TCP连接终止序列的发生。

close函数
close函数用来关闭套接字,并终止TCP连接

#include<unistd.h>int close(int sockfd);

close一个TCP套接字,将套接字标记关闭,然后立即返回到调用进程。

getsockname和getpeername函数
getsockname函数用于返回与某个套接字关联的本地协议地址
getpeername函数用于返回与某个套接字关联的外地协议地址

#include<sys/socket.h>int getsockname(int sockfd,struct sockaddr* localaddr,socklen_t* addrlen);int getpeername(int sockfd,struct sockaddr* peeraddr,socklen_t* addrlen);

什么情况下会使用这两个函数:
1 在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回本地IP地址和本地端口号
2 在以端口号为0调用bind函数后,getsockname返回系统赋予的端口号。
3 getsockname获取某个套接字的地址族
4 在以通配ip地址调用bind的TCP服务器上,某个客户的连接一旦建立(accept成功返回),getsockname就可以用于返回由内核赋予该连接的本地ip地址。套接字描述符参数必须是已连接套接字的描述符,而不是监听套接字的描述符。
5 当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途径是调用getpeername。
inetd调用accept返回两个值:已连接套接字描述符connfd,这是函数的返回值;客户的ip地址及端口号。inetd随后调用fork派生出inetd的一个子进程。当子进程调用exec执行真正的服务器程序时,子进程的内存映像被替换成新的服务器程序文件(例如Telnet服务器),Telnet服务器可以调用getpeername用于获取客户的ip地址和端口号。

//获取套接字地址族#include"unp.h"int sockfd_to_family(int sockfd){    struct sockaddr_storage ss;    socklen_t len;    len=sizeof(ss);    if(getsockname(sockfd,(SA*)&ss,&len)<0)        return(-1);    return(ss.ss_family);}
原创粉丝点击