unix套接字编程
这是学习[1]Unix网络编程第一卷的一点笔记。
图1 基本TCP客户——服务器程序的流程图
首先启动服务器,稍后的某个时刻启动客户端,它要连接到服务器上。假设客户端给服务器发送了一个请求,服务器处理这个请求,并给客户端发回一个响应。这个过程一直持续下去,直到客户端给服务器发送一个文件结束符,并关闭客户端连接,接着服务器也关闭服务器端的连接,或结束运行或等待一个新的客户端连接。
socket函数
为了执行网络I/O,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议类型。
#include <sys/socket.h>
int socket(int family, int type, int protocol); // succeed on returning non-negative descriptor
族 | 解释 |
---|---|
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | Unix域协议 |
AF_ROUTE | 路由套接口 |
AF_KEY | 密钥套接口 |
表1 socket函数的协议族(family)常值
类型 | 解释 |
---|---|
SOCK_STREAM | 字节流套接口 |
SOCK_DGRAM | 数据包套接口 |
SOCK_RAW | 原始套接口 |
表2 socket函数套接口类型(type)
AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY | |
---|---|---|---|---|---|
SOCKET_STREAM | TCP | TCP | Yes | ||
SOCK_DGRAM | UDP | UDP | Yes | ||
SOCK_RAW | IPv4 | IPv6 | Yes | Yes |
表3 socket函数的族与类型的组合
并非所有套接口family与type的组合都是有效的,表3给出了一些有效的组合和对应的真正协议。其中标"Yes"的项也是有效的,但还没有找到便捷的缩略词;而空白项则是不支持的。
socket函数在成功时返回一个小的非负整数,它与文件描述符类似,我们把它称为套接字接口描述字(socket descriptor),简称套接字(sockfd)。为了得到这个套接口描述字,我们只是制定了协议族(IPv4,IPv6或Unix)和套接口类型(字节流、数据包或原始套接口)。我们并没有指定本地协议地址或远程协议地址。
connect函数
TCP客户端用connect函数来建立一个与TCP服务器的连接
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen); // succeed on returning 0 else failure on returning -1
bind函数
函数bind给套接口分配一个本地协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen);
第二个参数是一个指向特定协议的地址结构的指针,第三个参数是该地址结构的长度。对于TCP,调用函数bind可以指定一个端口号,指定一个IP地址,可以两者都指定,也可以一个都不指定。
listen函数
函数listen仅被TCP服务器调用,它做两件事情:
1.当函数socket创建一个套接口时,它被假设为一个主动套接口,也就是说,它是一个将调用connect发起连接的客户套接口,函数listen将未连接的套接口转换成被动套接口,指示内核应接受指向此套接口的连接。根据TCP状态转换图,调用函数listen导致套接口从CLOSED状态转换到LISTEN状态。
2.函数的第二个参数规定了内核为此套接口排队的最大连接个数。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
accept函数
accept由TCP服务器调用,从已完成连接队列头返回下一个已完成连接。若已完成连接队列为空,则进程睡眠。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen);
参数cliaddr和addrlen用来返回连接对方进程的协议地址。addrlen是值-结果参数:调用前,我们将*addrlen所指的整数值置为由cliaddr所指的套接口地址结构的长度,返回时,此整数值即为由内核存在此套接口地址结构内的准确字节数。
如果函数accept执行成功,则返回值时内核自动生成的一个全新描述字,代表与客户的TCP连接。当我们讨论函数accept时,常把它的第一个参数称为监听套接口(listening socket)套接字(由函数socket生成的描述字,用作函数bind和listen的第一个参数),把它的返回值称为已连接套接口(connected socket)描述字。将这两个套接口区分开是很重要的。一个给定的服务器常常是只生成一个监听套接口且一直存在,直到该服务器关闭。内核为每个被接受的客户端连接创建一个已连接口(也就是说内核已为它完成TCP的三路握手过程)。当服务器完成某客户的服务时,关闭已连接套接口。
fork和exec函数
fork是unix中派生新进程的唯一方法
#include <unistd.h>
pid_t fork(void); //返回:子进程中为0,在父进程中为子进程id,-1——出错
fork在子进程返回0而不是父进程ID,一个原因是:子进程只有一个父进程,它总可以调用getppid来得到;而父进程有许多子进程,它没有办法来得到各子进程的ID。如果父进程想跟踪所有子进程的ID,它必须记住fork的返回值。我觉得还有一个就是fork在子进程中返回0使得子进程父进程的判断更加简单。
fork有两个典型应用:
1.一个进程可以为自己创建一个拷贝,这样,当一个拷贝处理一个操作时,其他的拷贝可以执行其他的任务。这是非常典型的网络服务器。
2.一个进程想执行其他的程序,由于创建新进程的唯一方法是调用fork,进程首先调用fork来生成一个拷贝,然后其中一个拷贝(通常为子进程)调用exec来代替自己去执行新程序。
并发服务器
pid_t pid;
int listenfd, connfd;
listenfd = Socket(...);
// fill in sockaddr_in() with server's well-known port
Bind(listenfd, ...);
Listen(listenfd, LISTENQ);
for(;;) {
// probably blocks
connfd = Accept(listenfd, ...);
if((pid==Fork())==0) {
Close(listenfd); // child closes listening socket which is set its reference value -1
doit(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}
每个文件或套接口都有一个访问计数,该访问计数在文件表项中维护,它表示当前指向该文件或套接口的打开的描述字个数。
Close函数
一般Unix函数close也用来关闭套接口,终止TCP连接。
#include <unistd.h>
int close(int sockfd); //返回:0——ok,-1——出错
TCP套接口的close其缺省功能是将套接口做上“已关闭”标记,并立即返回到进程。这个套接口描述字不能再为进程所用:它不能用作函数read或write的参数,但TCP将试着发送已排队待发的任何数据,然后按正常的TCP连接终止序列进行操作。
getsockname和getpeername函数
这两个函数或返回与套接口关联的本地协议地址(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);
一个使用TCP进行通讯的例子
例子来自[2]https://github.com/troydhanson/network
客户端
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *server = "127.0.0.1"; /* loopback */
int port = 6180; /* no significance */
int main(int argc, char *argv[]) {
char *buf = "hello, world!";
int buflen = strlen(buf), rc;
/**********************************************************
* create an IPv4/TCP socket, not yet bound to any address
*********************************************************/
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
printf("socket: %s\n", strerror(errno));
exit(-1);
}
/**********************************************************
* internet socket address structure, for the remote side
*********************************************************/
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(server);
sin.sin_port = htons(port);
if (sin.sin_addr.s_addr == INADDR_NONE) {
printf("invalid remote IP %s\n", server);
exit(-1);
}
/**********************************************************
* Perform the 3 way handshake, (c)syn, (s)ack/syn, c(ack)
*********************************************************/
if (connect(fd, (struct sockaddr*)&sin, sizeof(sin)) == -1) {
printf("connec