目录
网络字节序
socket编程接口
socket
bind
如果将进程比作一个房子,那套接字相当于是一扇门,通向与外界通信的通道。
在网络中,如何理解套接字呢,时刻记住套接字是为了标识互联网中的某一台主机上的某一个进程,因此它就是IP地址 + 端口号。
现在你完全可以把套接字当成一个结构体,里面包含了这台主机的IP地址和端口号。(只为了方便理解,实际中可能并不止如此)
在介绍套接字前,先来认识一下网络字节序
网络字节序
计算机的内存分为大端和小端存储数据,而不同的主机若想通信,则必须消除这个差别。
先来了解如下两个事实
发送主机通常将发送缓冲区的数据按照地址从低到高发出;
接受主机也是按照地址从低到高的顺序保存到接收缓冲区中;
因此,网络数据流规定:先发出数据的是低地址,后发出数据的是高地址。
TCP/IP协议规定,网络数据流采用大端序,也就是低地址高字节。
这样,小端机发送和接收数据时都需要将按大端序的数据转化为小端。
关于本地字节序转网络字节序,linux提供了以下系统调用
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
乍一看有四个接口,其实很好记,h代表host即本地,n代表net即网络,l即long表示长整型32位,s
即short表示短整型16位。
使用这些系统调用就可以实现本地字节序和网络字节序的相互转换
socket编程接口
先列出常用的socket api
#include <sys/types.h>
#include <sys/socket.h>// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
现在可能看不太懂,下面会讲解每个接口及其使用。
socket
第一个domain参数表示你将要使用的协议族,能选择的协议族定义在<sys/socket.h>
有许多可用的协议族,在这只重点谈AF_INET即用于网络通信的IPv4协议。
选择AF_INET代表着创建用于网络通信的套接字。
第二个type参数是套接字的类型,这里能用到的有SOCK_STREAM和SOCK_DGRAM,第一个是用于TCP的,能提供可靠的通信服务,第二个用于UDP,提供不可靠通信。
还有 SOCK_RAW即原始网络套接字,直接基于IP协议而不是基于TCP\UDP。
protocol参数传0即可。
返回值是一个一个文件描述符,通过该文件描述符即可像操作普通文件一样对套接字进行操作。
若创建失败则返回-1。
像对普通文件调用write和read一样对套接字进行读写。
这里指出,windows系统的套接字创建不会返回类似文件描述符这种描述文件的值,因为windows并不将套接字看作文件类型,而linux则通过“一切皆文件”的思想,为套接字的操作提供了与普通文件统一的接口。
int fd = socket(AF_INET, SOCK_DGRAM, 0);
这样就能创建一个UDP套接字并能通过fd来对它进行操作了,现在,走向下一步。
bind
前面把套接字比作是门,到目前为止我们已经造出了一扇门,但还不能使用,因为还没有将门“安装”在房子上。接下来我们要将套接字绑定到某个进程,即完成“安装”工作。
第一个sockfd参数决定你要绑定哪个套接字,就是上面介绍的socket函数的返回值。
要理解第二个参数,先来了解下面的知识:
套接字实际上并不是只有现在讲的网络套接字,有三种套接字,三种都使用的是同一套接口,
当使用原始网络套接字,传入sockaddr结构;使用基于TCP\UDP的网络套接字,传入sockaddr_in结构(in是inet),使用本地套接字,则需要传入sockaddr_un结构。
也就是说,不同种类的套接字需要在这第二个参数传入不同的结构体。
这实际上是一种多态的思想,操作系统的设计者期望使用统一的接口,对于不同的种类的套接字,提供不同的服务。而C语言并不支持多态,于是将sockaddr作为基类来设计接口。并且,上面提到的三个不同的结构,会有一个公共的16位的字段来告诉操作系统,自己是哪个种类的套接字。
因此,我们使用的网络套接字,在这里传入一个sockaddr_in的结构体即可。这个结构体中一定会包含IP地址和端口号这两个信息,这两个字段是需要用户自己填充的。
typedef uint16_t in_port_t;typedef uint32_t in_addr_t;
struct in_addr{in_addr_t s_addr;};struct sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of `struct sockaddr'. */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];};
可以看到 sockaddr_in结构包含端口号sin_port字段,sin_addr是一个结构体,这个结构体包含了s_addr即IP地址。
第三个字段是填充字段,不需要填充值。
现在解释第一个是什么字段
第一个字段中的__SOCKADDR_COMMON实际上是宏
#define __SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family
##表示将sa_prefix与family拼接起来,那么__SOCKADDR_COMMON (sin_);其实就是
sa_family_t sin_family;
这个字段也是需要我们填充的,它也表示协议家族,需要与创建套接字时传入的domain参数保持一致。
而第三个参数是为了让操作系统知道你传入的结构体大小有多大,以便它进行访问。
成功绑定则返回0,否则返回-1。
现在再理解一下绑定,绑定实际上就是把当前这个进程与用户输入的IP地址和端口绑定起来,别人访问这个IP地址,这个端口号,就是在访问这个进程。由此可以发现客户端不需要显式指定要绑定的端口和IP地址,也就是客户端不需要显式调用bind函数,而由操作系统分配某个端口号给该客户端。
关于填充IP地址和端口号,还有一些细节需要补充。
在填充端口号时,并不能确定主机是大端还是小端,绑定端口需要将本地字节序转为网络字节序,只需调用上面的htons即可。
绑定IP地址时,往往用户输入的是类似"127.0.0.1"这种形式,那就需要将这种字符串格式的IP地址转为32位并且转为网络字节序。对于这件事,linux也提供了相应的接口。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp);in_addr_t inet_addr(const char *cp);in_addr_t inet_network(const char *cp);char *inet_ntoa(struct in_addr in);struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);in_addr_t inet_lnaof(struct in_addr in);in_addr_t inet_netof(struct in_addr in);
可用
man inet
指令查看所有接口用法。