在类 Unix 的操作系统中,I/O 操作都是通过读写文件描述符(file descriptor)来进行的。文件描述符是类 Unix 系统中的一个重要概念。socket 操作的是网络 I/O,所以也沿用这种设计思路。socket 有对应的套接字描述符 sockfd。很多用于操作文件描述符 fd 的操作都可以用于操作 sockfd。就如常用的文件操作 write()/read() 就可以用于 sockfd,来实现数据的发送和接收。
在接着介绍 socket 之前,先跳出来了解一下文件描述符
文件描述符(File descriptor,后文用 fd 替代),是一个非负整数。当进程打开或者创建一个文件时,系统内核就会向进程返回一个 fd 用来指代这个文件。对文件读写时,就将 fd 作为参数传递给 write() / read() 函数。
在 Unix shell 中,文件描述符0与进程标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准错误相关联。
下面这张图描述了,进程、文件描述符以及文件间的关系:
继续介绍 socket
socket 地址
/*==================================
* socket 的地址结构,作为 bind(),
* connect(),accept() 的参数
*================================*/
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};
/*==================================
* sockaddr_in 是 IPv4 因特网地址,
* 是具体的 sockaddr。使用时需要强制
* 转换成 sockaddr
* sin_family = AF_INET
*================================*/
struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
/*==================================
* 4字节的 IPv4 地址
*================================*/
struct in_addr {
unsigned long s_addr;
};
这些地址是用于 socket 函数的。sockaddr_in 是专用于 IPv4 通信的。IPv6 的地址结构,这里未列出
socket 的地址分为 4类:
- AF_INET, IPv4 因特网域, 用于不同 pc 通过网络来通信
- AF_INET6, IPv6 因特网域
- AF_UNIX, (AF_LOCAL)UNIX 域, 用于同一台 pc 下不同进程间通信
- AF_UNSPEC, 未指定
可以看出 socket 不仅可以使用 ip 地址作为 sockaddr,还能使用其他协议族的地址。这正和 socket 的设计目标一致:同样的接口既可以用于计算机间通信还能用于计算机内通信
socket 函数
#include <sys/socket.h>
//建立连接
int socket(int domain, int type, int protocal);
int bind(int sockfd, const struct sockaddr *addr, socklent_t len);
int connect(int sockfd, const struct sockaddr *addr, socklen_t lent);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
//数据传输
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);
ssize_t sendmsg(int socklen, const struct msghdr *msg, int flags);
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//关闭连接
#include <unistd.h>
int close(int sockfd);
#include <sys/socket.h>
int shutdown(int sockfd, int how);
note:
1、strict 关键字, 用于告知编译器, strict 修饰的指针所指向的内容,只能通过这个指针修改。没有其他修改途径,有利于后面的代码优化
上面将 socket 相关函数分为了三类: 建立连接、传输数据、关闭连接。这么分的目的是为了和 TCP 传输的过程相对应。尽管 socket 并不仅仅用于 tcp 传输,这里为了和我们熟知的网络知识关联起来,就以 tcp 连接为例来介绍 socket
对于一个 c/s 模型的服务来说,client 和 server 间的通信可以简化以下两个部分:
client
- sockfd = socket( AF_INET, SOCK_STREAM, 0 )
- connect( sockfd, &serv_addr, sizeof(serv_addr) )
- wirte( sockfd, buffer, strlen(buffer) );
客户端创建一个 socket ,返回一个套接字描述符 sockfd。接着,connect() 函数,向服务器发起连接。这个步骤可以看成,client 和 server 进行 tcp 三次握手。
server
- sockfd = socket( AF_INET, SOCK_STREAM, 0 )
- bind(sockfd, &serv_addr, sizeof(serv_addr) )
- listen(sockfd, 1024);
- newsockfd = accept(sockfd, &cli_addr, sizeof(cli_addr) );
服务端创建一个 socket,返回一个套接字描述符 sockfd。bind( ) 将服务器的地址和其中一个端口绑定到 sockfd 上。调用 listen( ) ,开始在绑定的端口上监听来自客户端的连接。当有新的客户端连接到来时,调用 accept( ) 创建一个新的套接字描述符 newsockfd , 来处理这个新连接。之前的 sockfd 继续监听是否有新连接。
这个过程,可以与下面这张图对应起来
在前文中,介绍了最为典型的 socket 类型 Stream Sockets。stream socket 主要用于 tcp 传输
除此之外,socket 还有其他 3种类型
- Data gram Sockets ,用于 udp 传输
- Raw Sockets ,用来访问底层协议,主要用来开发新的协议
- Sequenced Packet Sockets,和 stream sockets 相似,但是它保留了边界