赞
踩
#include <sys/types.h> #include <sys/socket.h> int socket(int family, int type, int protocol) /** * func:创建通信端点(服务端)并返回一个描述符; * param family:指定通信域;选择用于通信的协议族; AF_INET IPv4 AF_INET6 IPv6 AF_LOCAL Unix域协议 AF_ROUTE 路由套接字 AF_KEY 密钥套接字 * param type:指定通信语义; SOCK_STREAM [TPC]提供有序的、可靠的、双向的、基于连接的字节流。 可能支持带外数据传输机制; SOCK_DGRAM [UDP]支持数据报(固定最大长度的无连接、不可靠消息); SOCK_SEQPACKET [有序分组]为最大固定长度的atagram提供一个顺序的、可靠的、基于双向连接的数据传输路径; 消费 者需要在每次输入系统调用时读取整个数据包; SOCK_RAW [原始套接字]提供原始网络协议访问; SOCK_RDM 提供不保证顺序的可靠数据报层; SOCK_NONBLOCK 在新打开的文件描述中设置O_NONBLOCK文件状态标志。 使用这个标志可以节省对fcntl(2) 的额外调用来实现相同的结果; * param protocol:当只有一个协议支持指定的套接字类型时,为0;其他需指定; IPPROTO_CP TCP传输协议 IPPROTO_UDP UDP传输协议 IPPROTO_SCTP SCTP传输协议 * return:成功返回新套接字的文件描述符,出错返回-1; * */
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) /** * func:将套接字连接到由addr指定的地址; * param sockfd:返回的文件描述符; * param addr:指向sockaddr结构的指针;若sockfd的类型是SOCK_DGRAM,则addr是默认情况下发送数据报的地 址,也是接收数据报的唯一地址; 若为SOCK_STREAM或SOCK_SEQPACKET,此调用将尝试与绑定到 addr指定地址的套接字建立连接; * param addrlen:传入结构体的大小; * return:成功返回0,失败返回-1; * */ - 套接字必须含有服务器的IP地址和端口号;客户在调用该函数不必调用bind函数,若需要,则内核会确定源IP地址,并选择 一个临时端口为源端口; - 该函数调用即TCP的三路握手过程,错误返回的几种情况: - 若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误,超时无响应; - 若客户的SYN的响应是RST(复位),则该服务器主机在我们指定的端口上没有进程在等待与之连接;返回ECONNREFUSED错误; - 若客户发出的SYN在中间的某个路由器上引发目的不可达ICMP错误(软错误); 【产生RST的三条件】: - 目的地为某端口的SYN到达,而该端口上没有正在监听的服务器; - TCP想取消一个已有连接; - TCP接收到一个根本不存在的连接上的分节;
指定本机地址获取时间;【OK】
指定本地子网上其主机ID并不存在的IP地址;当客户机发出ARP请求时,将不会收到;【TIMEOUT】
指定一个没有运行时间获取服务器程序的主机【RST】
指定英特网中不可达的IP地址【ICMP错误】
注意
- connect函数导致当前套接字从CLOSED状态,转移到SYS_SENT状态,若成功则再转移到ESTABLISHED状态,若connect失败
则该套接字不再可用,必须关闭,不能对这样的套接字再次调用connect函数;
- 当调用connect调用失败后,必须close当前的套接字描述符并重新调用socket;
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) /** * func:当使用socket创建套接字时,它存在于一个名称空间(地址族)中,但没有分配给它地址。 Bind()将addr * 指定的地址分配给套接字; * param sockfd:返回的文件描述符; * param addr:传入sockaddr_in结构体且需要进行强制转换为(sockaddr); serv_addr.sin_family = AF_INET; // 通信域 serv_addr.sin_port = htons(SERV_PORT); // 端口 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP * param addrlen:传入addr的长度; * return:成功返回0,失败返回-1; * */ 【bind可指定一个端口号或IP地址或两者都可指定】 - 【TCP客户没有捆绑端口】: 当调用connect或listen时,内核会为响应的套接字选择一个临时端口; - 如RPC服务器,内核为其选择临时端口,而端口通过RPC端口映射器进行注册,客户再connect之前,必须与端口映射器联系以获取它们的临时端口; - 【捆绑IP地址】: 对于TCP客户,为该套接字上发送的IP数据报指派了源IP地址; 对TCP服务器,就限定该套接字只接收那些目的地为该IP地址的客户连接; TCP客户通常不把IP地址捆绑到它的套接字上,当连接套接字时,内核将根据所用外出网络接口来选择源IP地址, 而外出接口这取决于到达服务器所需的路径; 若TCP服务器没有捆绑IP地址,内核及那个客户发送的SYN目的IP地址作为服务器的源IP地址;
IPv4一般使用来INADDR_ANY(0)来指定
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/**
由于INADDR_常值一般都为主机字节序定义,故对此类常值需要使用htonl
*/
IPV6一般为128位
struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any;
如果让内核来选择一个临时端口,bind无法获取,必须使用getsockname来获取;
当一个连接到达时,服务器调用getsockname获取客户的目的IP地址,服务器在根据这个客户连接所发往的IP地址来
处理客户的请求;
【好处】:把一个给定的目的IP地址解复用到一个给定的服务器进程时是由内核完成的;
必须细分一个分组到达接口和该分组的目的IP地址;若使用弱端系统模型,则意味着一个分组只要其目的IP地址能够标识
目的主机的某个网络接口即可,不一定是它的到达接口;
捆绑非通配IP地址只是限定根据目的IP地址来确定递送到套接字的数据报,而对于到达接口则未做任何限制,除非系统采
用强端系统模型;
#include <sys/socket.h> int listen(int sockfd, int backlog) /** * func:将套接字标记为被动套接字,即用于accept接收传入的连接请求的套接字; * param sockfd:返回的文件描述符; * param backlog:最大了连接数; - 内核为一个给定的监听套接字维护两个队列: 【未完成连接队列SYN_RCVD】:每个这样的SYN分节都对应某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手; 【已完成连接队列ESTABLISHED】:每个已完成TCP三路握手过程的客户对应其中一项; 当未完成连接队列时中创建一项时,来自监听套接字的参数本复制到将建立连接中;连接的创建机制是完成自动的, * return:成功返回0,失败返回-1; 【注意】:不要将backlog设置为0,若不想客户链接,关闭套接字即可; * */ 【该函数仅由TCP服务器调用,做以下两件事】: - 当socket创建一个套接字时,它时一个将调用connect发起连接的客户套接字;该函数把一个为连接的套接字转换成一个被动套接字,指示内核应接收指向该套接字的连接请求; 【调用导致套接字从CLOSED状态转换到LISTEN状态】
若三路握手正常,该项将从未完成连接队列中的队头移到已完成连接队列的队尾,当accept调用时,已完成连接队列中的队头项将返回给进程,若该队列未空,则进程被投入睡眠;
void Listen(int fd, int backlog){
char *ptr;
if((ptr) = getenv("LISTENQ") != NULL)
bakclog = atoi(ptr);
if(listen(fd, backlog) < 0)
exit(-1);
}
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
/**
* func:侦听套接字sockfd的挂起连接队列中提取第一个连接请求,创建一个新的连接套接字,并返回一个指向该套
* 接字的新文件描述符;
* param sockfd:返回的文件描述符;
* param addr:指向sockaddr结构的指针,为传出参数;
* param addrlen:为值-结果参数(可查看上一篇文章);
* return:成功返回文件描述符,失败返回-1;
* */
若无需客户的协议地址,可将addr和addrlen设置为NULL;
/** 运行时 */ #include "../Jxiepc/unp.h" #include <iostream> using namespace std; int main() { int lfd, cfd; socklen_t len; struct sockaddr_in s_addr, c_addr; char buf[128]; time_t ticks; /* 创建套接字 */ lfd = socket(AF_INET, SOCK_STREAM, 0); /* 绑定端口、IP */ bzero(&s_addr, sizeof(s_addr)); s_addr.sin_family = AF_INET; s_addr.sin_addr.s_addr = htonl(INADDR_ANY); s_addr.sin_port = htons(13); bind(lfd, (struct sockaddr*)&s_addr, sizeof(s_addr)); /* 设置监听个数 */ listen(lfd, 128); while (true) { /* 接收客户端 */ len = sizeof(c_addr); cfd = accept(lfd, (struct sockaddr*)&c_addr, &len); /* 获取客户端ip、端口 */ cout << "connect from " << inet_ntop(AF_INET, &c_addr.sin_addr, buf, sizeof(buf)) << ", port " << ntohs(c_addr.sin_port) << endl; /* 回写时间 */ ticks = time(NULL); snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks)); write(cfd, buf, strlen(buf)); close(cfd); } return 0; }
#include <unistd.h>
pid_t fork(void);
/*
* @func: 该函数调用一次返回两次,父进程中返回子进程的ID号,子进程中返回0;通过判断返回值来确定是否为子进程;
- 在子进程中,阔以通过getppid来获取父进程ID;父进程可以有多个子进程,但无法获取各个子进程的ID(需要对每个进行跟踪);
- 父进程中调用fork之前打开的所有描述符在fork返回后由子进程分享;
- 服务器可利用父进程中调用accept后调用fork,使用子进程来处理客户;
* return: 失败返回-1,成功返回:父进程返回子进程的ID以及子进程返回0;
*/
- 一个进程创建一个自身的副本,都可在另一个副本执行其他任务的同时处理各自的某个操作;
- 一个进程想要执行另一个程序,在子进程中调用exec把自身替换成新的程序;
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ...);
int execv(const char *pathname, char *const *argv[]);
int execle(const char *pathname, const char *arg0, ...);
int execve(const char *pathanem, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ...);
int execvp(const char *filename, char *const argv[]);
/**
上述函数中,只有函数错误时,才返回调用者;
只有execve是系统调用,其余为库函数;
*/
以上函数的区别
- 上图中上行的函数把新程序的每个参数字符串指定成exec的一个独立参数,并以空指针结束的可变参数;
下行以argv为参数数组,其中含有指向新程序各个参数字符串的所有指针;
- 左列两个函数以filename为参数,指定文件名;右两列四个函数以pathname为参数;
- 左两列函数不显示指定一个环境指针;它们使用外部变量environ的当前值构造一个传递给新程序的环境列表;
注意
在exec前打开的描述符在exec后依旧为打开状态,但可以通过fcntl设置FD_CLOEXEC禁止;
最简单的方法是使用fork创建子进程来服务每个客户;
- 当调用accept返回时,就使用fork由子进程来服务客户,父进程用来等待连接;
pid_t pid; int lfd, cfd; bind(lfd, ...); listen(lfd, 128); while(1) { cfd = accept(lfd, ...); if(pid == fork()) == 0) { close(lfd); doit(cfd); close(cfd); exit(0); } /** 当连接建立,accpet返回,调用fork,由子进程服务客户(cfd),父进程等待新连接,由于客户由子进程提供服务,故在父 进程中需要关闭已经连接的套接字; */ close(cfd); }
每个文件或套接字都由一个引用计数,维护在文件标项中,是当前打开着的引用描述符的个数;
socket返回后与lfd关联的文件引用数为1,accept与cfd关联的文件引用数也为1;在fork中,该两
个描述符在父进程与子进程间共享,故两个描述符的引用数变为2,而父进程中关闭cfd,只是将
引用计数从2减到1;
- 而真正的清理和资源释放要等到为0时才发生;
- 若父进程对accept返回的已连接套接字不调用close则将会耗尽可以描述符;
#include <unistd.h>
int close(int sockfd);
/**
@func: 关闭套接字,并终止TCP连接;
*/
上述中,close减少描述符的计数,有时并不会引发TCP终止序列;
若想要TCP发生FIN,则改用shutdown函数;
#include <says/socket.h> int getsockname(int sockfd, strict sockaddr *localaddr, socklen_t *addrlen); /** @func: - 在没有bind的TCP客户上,connect成功后,该函数用于返回由内核赋予连接的本地 IP地址和本地端口号; - 在以端口号0调用bind(由内核去选择本地端口号)后,该函数用于返回由内核赋予的本地端口号; - 可用于获取某个套接字地址族; - 在一个以通配地址调用bind的tcp服务器上,与某客户连接建立后,getsockname可用于返回由内核赋予该连接的本地IP地址; - 当一个服务器是由调用过accept的某个进程通过调用exec,该函数可获取客户身份; */ int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen); /** @func: */
描述符的传递:
- 调用exec的进程可以把这个描述符格式化成一个字符串,作为命令行参数传递;
- 约定exec之前,总是把某个特定描述符置为所接受的已连接套接字的描述符;
int sockfd_to_family(int sockfd) {
struct sockaddr_storage ss;
socklen_t len;
len = sizeof(ss);
if(getsockname(sockfd, (struct sockaddr*)&ss, &len) < 0)
return -1;
return ss.sin_family;
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。