//------------------------------------------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这个通用结构,因为它能够承载系统支持的任何套接字地址结构。