套接口编程基础
套接口也就是网络进程的ID。网络通信归根到底就是进程之间的通信。在网络中,每一个节点都有一个网络地址,即IP地址。两个进程通信时,首先要确定各自所在的网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而具体是这台计算机上的哪一个网络进程并不能确定。这时就需要端口了(port)。在一台计算机中,一个端口号依次只能分配给一个进程(端口号和进程之间一一对应)。所以使用端口号和网络地址就能唯一确定整个网络中的一个网络进程。
套接口地址结构就是用来存储网络地址和端口号信息的,它是一个结构体。
Linux中套接口的数据结构
在Linux中,每一种协议都有自己的网络地址数据结构,这些结构以sockaddr_开头,不同的是后缀表示不同的协议。
通用套接口地址数据结构
套接口数据结构总是通过指针来向一个套接口函数传递信息。为了做到协议无关性,在套接口函数中的套接口地址指针必须支持所有的协议族的套接口地址指针。通用套接口地址数据结构定义如下:
#include <sys/socket.h>
struct sockaddr
{
unit8_t sa_len ;
sa_family_t sa_family ; /* 协议族名 */
char sa_data[14] ;
}
connect函数原型:
int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);
对该函数的任何调用,将指向特定协议的套接口地址结构的指针类型,转换成指向通用套接口地址结构的指针。调用时注意其中的类型转换struct sockaddr不能省略,否则编译时会出现警告。
IPv4套接口地址数据结构
IPv4套接口地址结构以sockaddr_in命名,定义在< neitnet/in.h>头文件中。
#include <netinet/in.h>
struct in_addr /* 32位IP地址的结构 */
{
in_addr_t s_addr ;/* 32位IP地址 */
} ;
struct sockaddr_in
{
uint8 sin_len ;
sa_family_t sin_family ;
in_port_t sin_port ;/* 16位端口号 */
struct in_addr sin_addr ;
char sin_zero[8]; /* 备用的域,未使用 */
};
/*
* sockaddr_in: 结构中成员均以sin_开头。
* sin_len: 数据长度成员,固定长度为16字节,一般不用设置。
* sin_family: 协议族名IPv4为AF_INET。
* sin_port: TCP或UDP协议端口号。
*/
基本函数
字节排序函数
计算机在内存中存储方式有小端法和大端法。在网络中传输时使用的是大端法,而在个人主机中常用的是小端法。这就需要将主机字节序和网络字节序相对应。
主机字节序和网络字节序相互转换的函数:
#include <netinet/in.h>
uint16_t htons(uint16_t hostvalue) ;
uint32_t htonl(uint32_t hostvalue) ;
/* 返回网络字节序 */
----------
uint16_t ntohs(uint16_t hostvalue) ;
uint32_t ntohl(uint32_t hostvalue) ;
/* 返回主机字节序 */
/*
* htonl函数将32位整数由主机字节序转换为网络字节序。
* ntohl函数将32位整数由网络字节序转换为主机字节序。
* htons和ntohs为16位整数执行转换。
*
* h代表host,n代表network,s代表short(16位),l代表long(32位)。
* 一般使用htons和ntohs转换端口号,使用ntohl和htonl转换IP地址。
*/
字节操纵函数
在套接口编程中,经常需要读取一些结构体中的某几个字节,这就需要字节操纵函数。其中包括b开头的函数和mem开头的函数,前者由任何支持套接口函数的系统提供,后者由任何支持ANSIC库的系统提供。
#include <strings.h>
/*
* 此处的头文件也可以用<string.h>
*/
void bzero(void* dest, size_t nbytes) ;
void bcopy(const void* str, void* dest, size_t nbytes) ;
int bcmp(const void* ptr1, const void* ptr2, size_t nbytes) ;
void* memset(void* dest, int c, size_t nbytes) ;
void* memcpy(void* dest, const void* src, size_t nbytes) ;
int memcmp(const void* ptr1, const void* ptr2, size_t nbytes) ;
/*
* bzero函数将指定的起始地址设置为0字节;bcopy和memcpy函数用来复制;
* bcmp和memcmp用来比较;memset函数将目标中的指定数目的字节设置位值。
* 函数bcmp和memcmp中的参数ptr1和ptr2,如果前者大于后者,将返回大于0的值;如果相等,则返回0;如果前者小于后者,则返回小于0的值。
*/
地址转换函数
IP地址通常以点分十进制表示法来表示。但实际中的IP地址是一串二进制数据。这就需要转换了。
#include <arpa/inet.h>
int inet_aton(const char* cp, struct in_addr* inp) ;
/* 成功返回1,失败返回0(十进制到二进制) */
char* inet_ntoa(struct in_addr in) ;
/* 指向点分十进制字符串的指针(二进制到十进制) */
in_addr_t inet_addr(const char* straddr) ;
/* 成功返回32位的网络字节序地址,失败返回INADDR_NONE(常数)。
* 当返回这个常数时,就说明转换有问题。会返回-1。
*/
例:有struct_sockaddr_in类型的变量sin,将IP地址“162.105.12.145”存储到其中,可以使用inet_addr()。
sin.sin_addr.s_addr = inet_addr("162.105.12.145") ;
/* 得到的是已经按网络字节序地址的32位二进制地址 */
字节流读写函数
在Linux中一般的IO操作除了标准c中的操作,还有Linux系统的提供的IO(read和write),但由于套接口的缓冲区的原因(网络延迟或是缓冲约束,还有可能是本身调用read和write),会出现传送的数据出现不足值的情况。这时就需要修改read和write。
具体参考《深入理解计算机系统》第10章-10.4节。
书中提供了健壮的IO包(RIO)。
也可以上网搜完整的代码。请看优快云资源。
连接函数
在编写客户端和服务器程序时,当分配端口后,要建立连接,这一过程会用到连接函数。
创建套接口函数
#include <sys/socket.h>
int socket(int family, int type, int protocol) ;
/* 生成一个套接口描述字,返回非负描述字。则成功,若为负值,则失败。 */
/*
* 参数family指明协议族,参数type指明字节流类型,参数protocol一般为0。
*
* 参数family取值范围:
* AF_LOCAL UNIX协议族
* AF_ROUTE 路由套接口
* AF_INET IPv4协议
* AF_INET6 IPv6协议
* AF_KEY 密钥套接口
* 参数type的取值范围:
* SOCK_STREAM TCP套接口
* SOCK_DGRAM UDP套接口
* SOCK_PACKET 支持数据链路访问
* SOCK_RAM 原始套接口
*/
———-
绑定套接口函数bind,它的功能是为套接口分配一个本地协议地址,也就是本地IP地址与本地套接口的组合。调用bind可以指定一个端口号、一个IP地址,两者都指定或者两者都不指定。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen) ;
/* 返回0成功,返回-1失败,并设置全局errno */
/* 参数sockfd为套接口,参数myaddr是一个指向特定协议地址结构的指针,addrlen是地址结构的长度。 */
这个调用不是必须的。使用socket得到套接口后可以直接调用connect或者listen,这是内核会自动给套接口分配一个地址和端口号。只有在希望进程使用某个特定的网络地址和端口时,才会使用bind函数。如果使用telnet和远程机器通信,一般不关心用户的本地端口号,只调用connect就可以,它会检查套接口是否绑定,如果没有,就自己绑定一个没有使用的本地端口。
结构my_addr中的IP地址必须是所在主机的IP地址之一。对于客户机,它指定了套接口的源地址;对于服务器,套接口必须从该地址接受客户端的信息。
———-
创建套接口之后,就可以将客户端和服务端连接。
#include <sys/typess.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, int addrlen) ;
/* 成功返回0,失败返回-1,并设置全局errno */
/* 参数sockfd是系统调用socket()返回的套接口描述字,参数serv_addr是目的端口和IP地址的套接口,参数addrlen是目的套接口的长度。 */
在健壮的IO包中将socket函数和connect函数封装成了open_clientfd函数,很方便(具体的代码请自行搜索)。
———-
在编写服务器程序时,使用到函数listen。服务器进程不知道自己要与谁连接,因此它只是监听是否有客户进程要与它连接,然后响应这个连接请求,并对它做出处理,一个服务器进程可以同时处理多个客户进程的连接。一般函数listen与另一个系统调用accept一起使用,先用listen监听,然后用accept逐一处理各个连接。
#include <sys/socket.h>
int listen(int sockfd, int backlog) ;
/* 返回0成功,返回-1失败 */
/* sockfd为套接字,backlog为规定内核为此套接字排队的最大选择个数(通常设定为一个较大的值1024) */
在健壮的IO包中将socket、bind和listen函数封装成了open_listenfd函数(具体的代码请自行搜索)。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen) ;
/* 从已完成连接队列头返回一个已完成连接,如果已完成连接队列为空,则进程睡眠,这个函数有服务器调用。 */
/* 返回非负描述字,返回-1失败。 */
/* cliaddr为返回客户进程的协议地址,addrlen为返回客户进程的协议长度。 */
/* sockfd有socket函数生成,accept的返回值为已连接套接口描述字。 */
/* 若不需要客户的协议地址,可将第2、3个参数设为空。 */
一个服务器进程中系统调用通常如下
socket() ;
bind() ;
listen() ;
accept() ;
———-
TCP协议建立连接时使用“三段握手TWH”方式:
1) 客户端先用connect()向服务器发出一个要求连接的信号SYN1。
2) 服务器进程接收到这个信号后,发回应答信号ack1,同时这也是要求回答的信号SYN2。
3) 客户端收到信号ack1和SYN2后,再次发出应答信号ack2。
4) 服务器收到应答信号ack2,一次连接才算建立完成。
在这个过程中,服务器会收到两次信号SYN1和ack2,因此服务器进程需要两个队列保存不同状态的连接。刚接收到SYN1信号时,连接还未完成,这时的连接放在一个“未完成连接”的队列里。接收到ack2信号后,三段握手完成,这时的连接放在“已完成连接”的队列中,等待accept()调用。而listen()就用来监听信号。
———-
关闭套接口
#include <unistd.h>
int close(int sockfd) ;
/* 成功返回0,失败返回-1 */
关闭套接口后,以后这个套接口就不能再使用,但关闭时已经排队准备发送的数据仍会被发出。
shutdown()允许将某一方向的通信或者双向通信关闭。
int shutdown(int sockfd, in how) ;
/* 成功返回0,失败返回-1 */
/*
参数how:
0:不允许再接收。
1:不允许再发送。
2:不允许再接收和发送。
*/
shutdown并不是关闭套接口,而是关闭套接口的通信功能,该套接口仍是打开的。
———-
服务器为每个连接生成一个新进程来处理具体的连接。
可以用fork派生一个子进程,然后子进程调用exec族函数来进行处理。
#include<unistd.h>
pid_t fork(void) ;
/* 该函数返回两个值:父进程中返回子进程的ID,子进程中返回0,出错返回-1。 */
父进程在调用fork之前打开的所有描述字在函数fork返回之后都是共享的。所以可以在父进程中调用accept标识新的连接然后调用fork,随后子进程读协议连接套接口,而父进程则关闭已连接套接口。
———-
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) ;
当不调用bind或者是调用bind没有指定本地协议地址时,可以调用getsockname函数来返回内核分配给此连接的本地IP地址和端口号,还可以获得某套接口的协议族。当一个新的连接建立时,服务器也可以调用getsockname来获得分配给此连接的本地IP地址。
当服务器的一个子进程调用exec函数启动执行时,只能调用getpeername函数来获得客户端的IP地址和端口号。