您的位置:首页 > 产品设计 > UI/UE

apue和unp的学习之旅04——基本套接字api

2014-04-04 15:33 302 查看
//------------------------------------------socket 函数---------------------------------------

#include <sys/socket.h>

int socket (int  family, int  type, int  protocol);

                                              // ret: 若成功则返回非负描述符,出错则返回-1

//-----------------------------------------connect 函数---------------------------------------

#include <sys/socket.h>

int connect (int  sockfd, const struct sockaddr*  servaddr, socklen_t  addrlen);

                                          // ret: 若成功则为0,如出错则为-1

如果是TCP套接字(UDP不需要connect),调用connect函数将激发TCP的三路握手过程,而且仅仅在连接建立成功或出错才会返回。要知道的是,connect函数导致当前套接字从CLOSED状态(该套接字自从由socket函数创建以来的一直所处的状态)转移到SYN_SENT状态,若成功则再转移到ESTABLISHED状态。若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。

出错返回分为以下几种情况:

1.若TCP客户没有收到SYN分节(synchronous同步的意思)的响应,多次重发SYN分节给服务器后还是没有收到服务器端的TCP的响应,就会返回ETIMEDOUT错误。(我感觉种情况应该是服务器主机都没有开机,服务器的TCP自然不会理你,就会出现这种情况,又或者是尝试连接的IP不存在,当客户主机发出ARP请求,要求那个不存在的主机响应以其硬件地址时,将永远得不到ARP响应)

2.若对客户的SYN的响应是RST(表示复位reset),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(例如,服务器端的主机开机了,但是并没有运行服务器程序,也就是说该端口上没有正在监听的服务器程序),这是一种硬错误(hard error),客户一收到RST就马上返回ECONNREFUSED错误。

另外,当TCP想取消一个已有连接或者TCP接收到一个根本不存在的连接上的分节时也会发送一个RST的分节给客户。

3.若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable”的ICMP错误(指定了一个因特网中不可到达的IP地址),则认为是一种软错误(soft error)。客户主机内核保存改消息,并按照一定时间间隔继续发送SYN,若在某个规定时间后(BSD规定为75s)仍未收到响应,则把该保存的消息(即ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。

//-------------------------------------bind 函数--------------------------------------

#include <sys/socket.h>

int bind(int  sockfd, const struct sockaddr*  myaddr, socklen_t  addrlen);

                                                          // ret: 若成功则返回0,出错则返回-1

bind函数把一个本地协议地址赋予一个套接字,对于网际网协议,协议地址是32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。

对于TCP,调用bind函数可以指定一个IP地址,或指定一个端口号,也可以两者都指定,也可以两者都不指定。

比如不指定地址的IPv4一个例子:

struct sockadd_in servaddr;

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // INADDR_ANY是0,而s_addr 本身是32位的值

bind(socket, (sockaddr*)&servaddr, sizeof(servaddr));

在IPv6的例子,因为128位的IPv6地址是存放在一个结构中

struct sockaddr_in6  servaddr;

servaddr.sin6_addr = in6addr_any;

系统预先分配in6addr_any变量并将其初始化为常值IN6ADDR_ANY_INIT.头文件<netinnet/in.h>中含有in6addr_any的extern声明。要注意所有<netinnet/in.h>中定义的INADDR_常值都是按照主机字节序定义的,我们应该对任何这些常值都是用htonl。

如果不指定端口号,因为bind函数的第二个参数为const,所以第二个参数无法返回实际使用的端口号。为了得到内核所选择的的这个临时端口值,必须调用函数getsockname来返回协议地址。

//-------------------------------------listen 函数--------------------------------------

#include <sys/socket.h>

int listen(int  sockfd,int  backlog);

                                                              // ret:若成功则为0,失败则为-1

1.listen 函数仅仅由TCP服务器调用

2.当socket函数创建一个套接字时,它被假设为一个主动套接字,listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应当接受指向该套接字的连接请求。调用listen导致套接字从CLOSED状态到LISTEN状态。

3.TCP为监听套接字维护的两个队列,未完成连接队列,每个这样的SYN分节对应其中的一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程,这些套接字(哪些套接字???)处于SYN_RECV状态。

已完成连接队列,每个已完成TCP三路握手过程的客户对应其中一项,这些套接字处于ESTABLISHED状态。两队列之和不超过backlog(不同os对它的处理不同)。

指定较大的backlog值的理由在于:随着客户的SYN分节的到达。未完成连接队列中的项数可能增长,它们等着三路握手的完成。

5.当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。

6.不要把backlog设置为0,因为不同的实现对此有不同的解释,若不想让任何客户连接到你的监听套接字上,那就关闭掉监听套接字。

7.在三路握手正常完成的前提下(也就是说没有丢失分节,从而没有重传),未完成连接队列中的任何一项在其中存留的时间就是一个RTT,而RTT的值取决于特定的客户与服务器。

8.为避免修改backlog的值而重新编译,可以先设定一个默认值,不过允许通过命令行选项或环境变量覆写该默认值。如下的listen的包裹函数:

void Listen(int  fd,int  backlog)

{

     char*    ptr;

     if((ptr = getenv("LISTENQ")) != NULL)

     {

          backlog = atoi(ptr);

     }

     if(listen(fd, backlog))

     {

          err_sys("listen error");

     }

}

9.当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送RST,这么做是因为:这种情况是暂时的,客户TCP将重新发送STN,期望不久能在这些队列中找到可用空间,要是服务器TCP立即响应以一个RST,客户的connect调用就会立即返回一个错误,强制让应用进程处理这种情况,而不是让TCP的正常重传机制来处理。

10.在三路握手完成之后,但在服务器调用accept之前到达的数据应由服务器端的TCP排队,最大数据量为相应已连接套接字的接收缓冲区大小。

//-----------------------------------accept 函数----------------------------------------

#include <sys/socket.h>

int  accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen);

参数cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址。其中addrlen参数是个值结果参数,调用前,我们将由*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核

存放在该套接字地址结构内的确切字节数。如果我们队返回的客户协议地址簿感兴趣,那么可以把cliaddr和addrlen均置为空指针。

// -----------------------------简单介绍6个exec 函数(非套接字api)---------------------------------

exec函数把当前进程映像替换成新的程序文件,进程id并不改变,而且该新程序通常从main函数开始执行。

#include <unistd.h>

int execl (const char* pathname, const char* arg0,...(char*)0 );

int execv (const char* pathname, char* const argv[] );

int execle (const char* pathname, const char *arg0,...(char*)0,char* const envp[]);

int execve (const char *pathname, char* const argv[], char* const envp[]);

int execlp (const char* filename, const char* arg0,.../*(char*)0 */);

int execvp (const char* filename, char* const argv[]);



1.tip:记住的方法是理解它们的命名规则,比如有字母l的函数,那么参数里带有字符串列表按顺序逐个列出,且最后一个必须是空指针,作为末尾的标记。

有v的有字符串指针数组取代上述逐个列出的方法,

有e的则有环境参数,

有p字母的则都是第一个参数为文件名而不是路径。

2.其中只有execve是内核中的系统调用,其他5个都是都是直接或间接调用execve的库函数、

3.execlp和execvp这2个函数指定一个filename参数,exec将使用当前的PATH环境变量把该文件名参数转换成一个路径名,然而一旦这个文件名参数中有出现一个斜杠/,就不再使用PATH环境变量。

4.左两列4个函数不显示指定一个环境指针,它们使用外部变量environ的当前值来构造一个传递个i新程序的环境列表,右列2个函数显式指定一个环境列表,其envp指针数组也必须以一个空指针结束。

5.进程通常在调用exec之前打开着的描述符通常跨exec继续保持打开,之所以说是“通常”,因为本默认行为可以通过fcntl设置FD_CLOEXEC描述符标志禁止掉。

//------------------------------close 函数(非套接字api)-----------------------------

#include <unistd.h>

int close(int sockfd);

                                                // ret 若成功则为0,若出错则为-1

close一个TCP套接字的默认行为是把该套接字标记为已关闭,然后立即返回到调用进程,该套接字不能再作为read或write的参数了。然后TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。

//-------------------------------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.getsockname返回某个套接字关联的本地协议地址,getpeername()返回与某个套接字关联的外地协议地址。

2.这两个函数的其中addrlen都是值结果参数。

3.在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回内核赋予该连接的本地IP地址和本地端口号。

4.在以端口号0调用bind(告知内核去选择本地端口号)后,getsockname用于返回由内核赋予的本地端口号或者在一个以通配ip地址调用bind的TCP服务器上,与某个客户的连接一旦建立(accept成功返回),getsockname就可以用于返回由内核赋予该连接的本地ip地址,只要把返回的sockfd作为getsockname的参数即可,而不是监听套接字。

5.一个典型的并发服务器程序的轮廓

pid_t pid;

int listenfd,connfd;

listenfd = Socket(...);

Bind(listenfd,...);

for(;;)

{

    connfd = Accept(listenfd,...);

    if((pid = Fork()) == 0)            // child

    {

        Close(listenfd);

        doit(connfd);

        Close(connfd);

        exit(0);

    }

    Close(connfd);                     // parent

}

子进程起始于父进程的内存映像的一个副本,父进程中的那个套接字地址结构在子进程中也可以使用,那个已连接套接字描述符也是如此(因为描述符在父子进程之间是共享的)。然而当子进程调用exec执行真正的服务器程序(譬如说Telnet服务器程序)时,子进程的内存印象被替换为新的Telnet服务器程序文件,也就是说包含对端地址的那个套接字地址结构就此丢失)不过那个已经连接套接字描述符跨exec继续保持开放。Telnet服务器首先调用的函数之一便是getpeername,用于获取客户的IP地址和端口号。

6.getsockname可以用于获取某个套接字的地址族,比如

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

}

代码解释:因为不知道要分配的套接字地址结构类型,我们于是采用sockaddr_storage这个通用结构,因为它能够承载系统支持的任何套接字地址结构。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息