第五章 Linux网络编程基础API
第五章主要介绍的是Linux网络编程中最常用的API,也就是系统为我们提供的网络编程接口——Socket。
Socket可以看成是一个可读,可写,可控制, 可关闭的文件描述符,同时也是我们控制系统内核的一个接口,它为我们提供了许多功能的函数。书中首先介绍的是Socket的地址API。
Socket地址API的内容:
1.基本知识:主机字节序与网络字节序。
主机字节序就是我们常说的小端模式,高字节放高位,低字节放低位;网络字节序就是我们常说的大端模式,与小端模式相反。如上所说,主机端常用小端模式存储数据,所以主机字节序==小端模式;网络字节序常用大端模式存储数据,所以网络字节序==大端模式。
Linux提供许多函数用于两种模式的转换。
2.通用与专用的Socket地址。
(1)Socket通用地址结构体为sockaddr,能够指明Socket的协议族和地址值;当地址值不够用时,还有sockaddr_storage这个新的结构体,功能上一致。
(2)专用的Socket地址是相对于各种协议而言的,比如sockaddr_in,sockaddr_in6就是IPv4于IPv6对应的socket地址结构体。
3.IP地址转换函数
Linux提供的地址转换函数支持点分十进制,点分十六进制和字符串之间的相互转换。
#include<arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp, struct in_addr* inp);
char* inet_ntoa(struct in_addr in);
inti net_pton(int af, const char* src, void* dst);
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);
Socket基础API
Socket基础API用于创建对应协议的连接,他有以下常见的函数
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
int connect(int sockfd,const struct sockaddr* serv_addr,socklen_t addrlen);
int close(int fd);
int shutdown(int sockfd, int howto);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
(1)创建Socket.
创建Socket的函数为
int socket(int domain, int type, int protocol);
它可以指明socket的协议族,服务类型,以及具体的协议。
(2)命名socket
将一个socket与一个socket地址绑定,称为给socket命名。
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
bind函数将my_addr所指的socket地址分配给未命名的sockfd文件描述符,再指明长度addrlen。
(3)监听socket
listen函数将被命名的socket放入监听队列,监听客户端的连接。
int listen(int sockfd, int backlog);
(4)接受连接
accept函数从监听队列中接受一个socket连接
int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
sockfd是执行过listen的监听socket,addr参数用来获取被接受连接的服务端socket地址。
(5)发起连接
服务器端可以用listen被动地接受连接,那么客户端需要通过connet函数主动发起连接
int connect(int sockfd,const struct sockaddr* serv_addr,socklen_t addrlen);
sockfd由系统调用返回一个socket,serv_addr参数是服务器监听的socket地址,addrlen为长度。
(6)关闭连接
int close(int fd);
int shutdown(int sockfd,int howto);
注意close与shutdown的区别:close不是立即关闭一个连接,而是将fd的引用值减一,当fd的引用值为0时,才真正关闭连接,每次fork会将fd值+1;shutdown函数用于立即终止连接,howto为可选操作。
(7)数据读写
数据读写才是传输中的重头戏,以TCP数据流读写为例,有下面的代码:
服务器端:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define BUF_SIZE 1024
int main(int argc, char **argv) {
if(argc <= 2) {
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(sock, 5);
assert(ret != -1);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(int);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd < 0) {
printf("errno is: %d\n", errno);
}
else {
char buffer[BUF_SIZE];
memset(buffer, 0, sizeof(buffer));
ret = recv(connfd, buffer, BUF_SIZE - 1, 0);
printf("got %d bytes of normal data '%s'\n", ret, buffer);
memset(buffer, 0, sizeof(buffer));
ret = recv(connfd, buffer, BUF_SIZE - 1, MSG_OOB);
printf("got %d bytes of oob data '%s'\n", ret, buffer);
memset(buffer, 0, sizeof(buffer));
ret = recv(connfd, buffer, BUF_SIZE - 1, 0);
printf("got %d bytes of normal data '%s'\n", ret, buffer);
close(connfd);
}
close(sock);
return 0;
}
客户端:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(int argc, char **argv) {
if(argc <= 2) {
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in server_address;
bzero(&server_address, sizeof(server_address));
server_address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &server_address.sin_addr);
server_address.sin_port = htons(port);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
if(connect(sockfd, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
printf("connection failed\n");
}
else {
const char* oob_data = "abc";
const char* normal_data = "123";
send(sockfd, normal_data, strlen(normal_data), 0);
send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
send(sockfd, normal_data, strlen(normal_data), 0);
}
close(sockfd);
return 0;
}
UDP的发送和接收函数与TCP的类似,区别在于多了两个参数用来表示对端的socket地址结构。
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr* src_addr,socklen_t* addrlen);
ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr* dest_addr,socklen_t* addrlen);
API中还定义了两个不太常用的通用数据读写系统调用recvmsg和sendmsg,还有sockatmark函数用来判断sockfd是否处于带外标记状态,用getsockname和getpeername函数获取本端和对端的socket地址结构,使用较为简单。
(8)Socket的设置。
#include<sys/socket.h>
int getsockopt(int sockfd, int level, int option_name, void *option_value, socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void *option_value, socklen_t* option_len);
在参数中,level指定了要操作哪个协议的属性,option_name则指定选项的名字,这两项都有固定的搭配,接下来的option_value和option_len是值的大小和长度。值得注意的是,有些选项需要在TCP连接建立之前就设置好,即在调用listen和connect函数之前就设置,这是因为某些选项是TCP连接时需要互相协商的选项,而调用这两个函数时表示已经开始连接或完成连接。
选项种类有很多,常用的有:SO_REUSEADDR选项用来重用TCP端口;SO_RCVBUF和SO_SNDBUF选项用来设置接收及发送缓冲区大小,SO_RCVLOWAT和SO_SNDLOWAT设置缓冲区的低水位标记,意思是如果缓冲区的可读数据或可写空间大于低水位标记系统才通知应用程序从缓冲区读出数据或写入数据,一般默认低水位标记为1字节;SO_LINGER选项用来控制close在关闭TCP时的行为。
(9)网络信息API
socket也提供了几个网络信息API,分别用来根据主机名和ip地址获取主机信息,根据名称或端口获取服务信息,其函数原型如下
#include<netdb.h>
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
需要指出的是,以上四个函数都是不可重入的,即非线程安全的,不过netdb.h头文件也给出了他们的可重入版本,就是在函数名后加上_r。