套接字编程

套接字地址结构

大多数套接字哈桉树都需要一个指向套接字地址结构的指针作为参数。每个协议族都定义它自己的套接字地址结构。这些结构的名字均以sockaddr_开头,并以对应每个协议族的唯一后缀结尾。

IPv4套接字地址结构

IPv4套接字地址结构以sockaddr_in命名,定义在<netinet/in.h>头文件中。

struct in_addr{
    in_addr_t  s_addr;
}

struct sockaddr_in{
    uint8_t          sin_len;    //结构体长度为16
    sa_family_t      sin_family; //AF_INET
    in_port_t        sin_port;   //16位TCP或UDP端口号

    struct in_addr   sin_addr;   //32位IPv4地址

    char             sin_zero[8];//未使用
}

【说明】

  1. sin_len是增加对OSI协议的支持而随4.3BSD-Reno添加的。
  2. 在此之前,第一个成员是sin_family,是一个无符号短整数

通用套接字地址结构(IPv4)

当作为一个参数传递进任何套接字函数时,套接字地址结构总是以引用形式(也就是以指向该结构的指针)来传递。

<sys/socket.h>头文件中定义一个通用的套接字地址结构,如下

struct sockaddr{
    uint8_t         sa_len;
    sa_family_t     sa_family;
    char            sa_data[14];
}

具体使用如:

struct  sockaddr_in serv;

bind(sockfd,(struct sockaddr*) &serv,sizeof(serv));

 

值-结果参数

当往一个套接字函数传递一个套接字地址结构时,该结构总是以引用形式来传递,也就是说传递的是指向该结构的一个指针。该结构的长度也是作为一个参数来传递的,不过其传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程。

(1) 从进程到内核传递套接字地址结构的函数有3个:bind、connect和sendto。这些函数的一个参数是指向某个套接字地址结构的指针,另一个参数是该结构的整数大小,例如:

 

struct sockaddr_in serv;

connect(sockfd,(SA*) &serv,sizeof(serv));

(2) 从内核到进程传递套接字地址结构的函数有4个:accept、recvfrom、getsockname和getpeername。这4个函数的其中两个参数是指向某个套接字结构的指针和指向表示该结构大小的整数变量的指针,例如:

struct sockaddrr_un cli;
socklen_t len;

len = sizeof(cli);
getpeername(unixfd,(SA)* &cli,&len);

网络字节序的转换有以下4个函数:

#include<netinet/in.h>
uint16_c htons(uint16_t host16bitvalue);
uint32_c htonl(uint32_t host32bitvalue);
                                        //均返回:网络字节序的值
uint16_c ntohs(uint16_t net16bitvalue);
uint32_c ntohl(uint32_t net16bitvalue);
                                        //均返回:主机字节序的值

其中,h代表的是host,n代表了network,s代表short,l代表long。

 

inet_aton、inet_addr和inet_ntoa函数

  1. 上述三个函数在点分十进制(192.168.11.103)与它长度为32位的网络字节序二进制值间转换IPv4地址。
  2. 相对来说,有两个较新的函数 inet_pton和inet_ntop对于IPv4和IPv6地址都适用。
#include<arpa/inet.h>

//返回:若字符串有效则为1,否则为0
int inet_aton(cosnt char *strptr,struct in_addr *addrptr);
                               
//返回:若字符串有效则为32位二进制网络字节序的IPv4地址,否则为INADDR_NONE           
in_addr_t inet_addr(const char *strptr);

//返回:指向一个点分十进制数串的指针
char *inet_ntoa(struct in_addr inaddr);

第一个函数inet_aton将strptr所指的C字符串转换成一个32位的网络字节序二进制值,并通过指针addrptr来存储。若成功则返回1,否则返回0。

  • 有一个特征:如果addrptr指针为空,那么该函数仍然对输入的字符串执行有效性检查,但是不存储任何结果。

 

inet_pton和inet_ntop函数

这两个函数是随IPv6出现的新函数,对于IPv4和IPv6地址都适用。函数名中p和n分别代表表达(presentation)和数值(numeric)。地址表达格式通常是ASCII字符串,数值格式则是存放到套接字地址结构中的二进制。

#include<arpa/inet.h>

//返回:若成功则为1,若输入不是有效的表达格式则为0,若出错则为-1
int inet_pton(int family,const char *strptr,void *addptr);

//返回:若成功则为指向结果的指针,若出错则为NULL
const char *inet_ntop(int faamily,const void *addrptr,char *strptr,size_t len);

这两个函数的family既可以是AF_INET,也可以是AF_INET6。如果以不被支持的地址族作为family参数,这两个函数就都返回一个错误,并将errno置为EAFNOSUPPORT。

  • 第一个函数尝试转换由strptr指针所指的字符串,并通过addrptr指针存放二进制结果。若成功则返回值为1,否则如果对所指定的family而言输入的字符串不是有效的表达格式,那么返回值为0。
  • inet_ntop进行相反的转化,从数值格式(addrptr)转换到表达格式(strptr)。len参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。
    • inet_ntop函数的strptr参数不可以是一个空指针。调用者必须为目标存储单元分配内存并指定其大小。调用成功时,这个指针就是该函数的返回值。

 

 

基本TCP套接字编程

socket函数

#include<sys/socket.h>

返回:若成功为非负描述符,若出错则为-1
int socket(int family,int type,int protocol);

 

connect函数

#include<sys/socket.h>

//返回:若成功则为0,若出错则为-1
int connect(int sockfd,const struct sockaddr* servaddr,socklen_t addrlen);
  • 客户在调用函数connect钱不必非要调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。如果是TCP套接字,则connect函数将激发TCP三次握手过程,而且仅在连接建立成功或出错才返回,通常有以下几种错误返回:
  1. TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误。
  2. 若对客户的SYN响应是RST(复位),则表示该服务器主机在我们指定的端口上没有进程在等待与之连接。客户一接受到RST就马上返回ECONNREFUSED错误。
  3. 若客户发出的SYN在中间某个路由器上引发了一个“destination unreachable”(目的地不可达)ICMP错误,则认为是一个软错误。客户机内核保存该信息,并按第一种情况所述的时间间隔继续发送SYN。若某个时间内仍未收到响应,则把保存的信息作为EHOSTUNREACH或ENETUNREACH错误返回给进程。

 

bind函数

bind函数把一个本地协议地址赋予了一个套接字。

#include<sys/socket.h>


//返回:若成功为0,若出错则为-1
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen)

第二个参数是一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度。对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,或者两者都指定,或者两者都不指定。

  • 服务器在启动时捆绑它们众所周知的端口。
  • 如果TCP客户或者服务器均为调用bind捆绑端口,当调用connect或listen时,内核要为相应的套接字选择一个临时端口。
  • 进程可以把一个特定的IP地址捆绑到它的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一
  • 通常,通配地址由常值INADDR_ANY来指定,其值一般为0;它告知内核去选择IP地址;使用如下:
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl(INADDT_ANY);

 

 

  • 通常,进程捆绑非通配IP地址到套接字上的常见例子是在为多个组织提供Web服务器的主机上。每个组织都有各自的域名,譬如www.organizaation.com;其次,每个组织的域名都映射到不同的IP地址,不过通常在同一个子网上
  • 我们在连接时候也可使用另外一种方法:运行捆绑通配地址的单个服务器,当一个连接到达时,服务器调用getsockname函数获取来自客户的目的IP地址;然后服务器根据这个客户连接所发往的IP地址来处理客户的请求。

 

listen函数

listen函数仅由TCP服务器调用,它做两件事:

  1. 当socket函数创建一个套接字时,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。
  2. 本函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数。
#include<sys/socket.h>


//返回:若成功为0,若出错则为-1
int listen(int sockfd,int backlog);

【注】本函数通常应该在调用socket和bind两个函数之后,并在调用accept函数之前调用。

对于其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列:

  1. 未完成连接队列,每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD状态。
  2. 已完成连接队列,每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。

 

accept函数

accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式)。

#include<sys/socket.h>


//若成功则为非负描述符,若出错则为-1
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen);

如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。

第一个参数为监听套接字描述符(由socket创建,随后用作bind和listen的第一个参数的描述符),称它的返回值为已连接套接字描述符。同时,我们要区分这两个套接字:

  • 一个服务器通常仅仅创建一个监听套接字,它在服务器的生命周期内一直存在。
  • 内核为每个由服务器进程接受的客户连接创建一个已连接套接字,当服务器完成对某个给定客户连接时,相应的已连接套接字就被关闭了。

 

fork和exec函数

fork函数是Unix中派生新进程的唯一方法。

#include<unistd.h>


//返回:在子进程中为0,在父进程中为子进程ID,若出错则为-1
pid_t fork(void);

调用该函数一次,返回两次结果。它在调用进程(父进程)中返回一次,返回值是新派生进程(称为子进程)的进程ID号;在子进程又返回一次,返回值为0。因此,返回值本身告知当前进程是子进程还是父进程。

父进程中调用fork之前打开的所有描述符在fork返回之后由子进程分享。我们将看到网络服务器利用了这个特性:父进程调用accept之后调用fork。所接收的已连接套接字随后就在父进程与子进程之间共享。通常,子进程接着读写这个已连接套接字,而父进程则关闭这个已连接套接字。

fork有两个典型用法:

  1. 一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。这个网络服务器的典型用法。
  2. 一个进程想要执行另一个程序。既然创建新进程的唯一方法就是调用fork,该进程于是首先调用fork创建一个自身的副本,然后其中一个副本(通常为子进程)调用exec把自身替换成新的程序。

 

getsockname和getpeername函数

这两个函数或者返回与某个套接字关联的本地协议地址(getsockname),或者返回与某个套接字关联的外地协议地址(getpeername)。

#include<sys/socket.h>

//均返回:若成功则为0,若出错则为-1
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用于返回由内核赋予的本地端口号。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值