基本的套接字编程
socket函数
#include<sys/socket.h>
int socket(int family, int type, int protocol);
//返回:若成功则为非负描述符,若出错则为-1
其中family是协议族,type是套接字类型,protocol为某个协议组合,设为0的话是取family和type组合的系统默认值。
常用的family值有:AF_INET(IPV4)、AF_INET6(IPV6)。
常用的type值有:SOCK_STREAM(TCP|SCTP)、SOCK_DGRAM(UDP)、SOCK_SEQPACKET(SCTP)。
protocol 若设置为0,以选择所给定family和type组合的系统默认值
connect函数
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
sockfd是由socket函数返回的套接字描述符,第二个,第三个参数分别是一个指向套接字地址结构的指针和该结构的大小,
客户端调用connect前不必非得调用bind。如果是TCP套接字,connect会在三路握手建立成功或出错时才返回,出错返回可能是:
*TCP*客户没收到*SYN*分节,返回超时错误。
服务器端的响应为*RST*,表明该服务器没有进程在监听此端口。产生*RST*的三个条件:*SYN*到达的端口没有监听进程;*TCP*想取消一个已有连接;*TCP*接到不存在的连接上的分节。
客户发出的*SYN*被路由认为不可到达。客户内核会重发,若超时后未成功则返回错误。
connect导致套接字状态从CLOSED转移到SYN_SENT,若成功再转移到ESTABLISHED,若失败则该套接字不再可用,必须关闭。这样的套接字不可再connect,需要close后重新调用socket获取。
bind函数
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
第二个参数是一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度
bind是将本地协议地址绑定到套接字上。绑定时未指定的IP地址和端口会由内核选择。
TCP客户通常不绑定,在connect时,内核选择源IP地址。TCP服务器如果没有绑定IP地址,内核将在建立连接时把客户发送的SYN的目的IP设为服务器的源IP地址。
IPV4的通配地址为INADDR_ANY,IPV6的通配地址为IN6ADDR_ANY_INIT。
没有指定bind的端口号时,需要用getsockname来返回协议地址,再获取端口号。
进程捆绑非通配IP到套接字上的常见例子是同一主机上提供多个Web服务器,每个服务器的副本捆绑不同的IP。另一种方法是单个服务器进程绑定通配地址,当客户连接到达时,用getsockname获取客户的目的IP,再处理相应的服务请求。
listen函数
#include<sys/socket.h>
int listen(int sockfd, int backlog);
仅由TCP服务器调用:
socket创建的套接字为主动套接字,listen时会转换成被动套接字,从CLOSED转换为LISTEN。
第二个参数backlog规定了内核应该为相应套接字排队的最大连接个数。
内核要为监听套接字维护两个队列:
未完成连接队列,客户的SYN已到达服务器,未完成三路握手,处于SYN_RCVD状态。
已完成连接队列,完成握手的连接,处于ESTABLISHED状态,等待被accept。
不要把backlog设为0。
accept函数
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
第二个参数是客户端对应的structaddr_in 的结构,第三个就是该结构的大小,通过这个函数我们
如果传了可以获得客户端的structaddr_in 的具体内容
服务器用accept从已完成连接队列头返回下一个已完成连接。
用clientaddr的sin_addr和sin_port字段查看客户的IP和端口。
fork和exec函数
#include<unistd.h>
pid_t fork(void);
在unix中fork是派生新进程的唯一方法
fork调用一次返回两次,在他的调用进程中返回一次,返回值是新派生进程的进程id,在子进程又返回一次,返回值为0,因此返回值本身告知当前进程是子进程还是父进程, fork在子进程返回0而不是父进程的进程ID的原因在于:任何子进程只有一个父进程,而且子进程总是可以通过getppid取得父进程的进程id。相反,父进程可以有许多子进程,而且无法获得各个子进程的进程ID,如果父进程想要跟踪子进程的进程ID,那么他必须记录每次调用fork的返回值。
fork有两个典型用法:
1、一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。这是网络服务器的典型用法。我们将在本书后面看到许多这样的例子。
2、一个进程想要执行另一个进程。既然创建新进程的唯一办法是fork,该进程于是首先调用fork创建一个自身的副本,然后其中一个副本调用exec吧自身替换成新的程序。
并发服务器
pid_t pid;
int listenfd, connfd
listenfd = Socket(...);
Bind(listenfd,...);
Listen(listenfd, LISTENQ);
for( ; ; ) {
connfd = Accept(listenfd, ...);
if ( ( pid =Fork() ) == 0) {
Close(lisenfd);
doit(connfd);
Close(connfd);
exit(0);
}
}
多进程的并发服务器,监听进程在fork后要关掉connfd,子进程在fork后要关掉listenfd,否则描述符的引用计数不为0,在close时不会真正关闭。在fork后listen和connfd的引用计数都都+1了,子进程进入那个if语句块,主进程则不进入。然后我们那个这个连接套接字就可以对连接的这个做一些事了。
close 函数
#include<unistd.h>
int close(int sockfd);
close后的套接字描述符不能再使用,但TCP将尝试发送已排队的任何数据,完毕后进行正常的终止序列。
#include<sys/socket.h>
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
前者返回套接字的本地端的地址结构,后者返回外地端的地址结构。只知道connfd的时候,获取客户身份的唯一途径就是调用getpeername。
exec后的程序获取connfd的方法:
将描述符转为字符串,作为命令行参数传给新程序。
约定在调用exec前,总把某个特定描述符设置为connfd,如将0、1、2设置为connfd。