- socket一般指套接字。 所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
- socket地址API:ip:port唯一的表示了TCP通信的一端
- socket基础API:创建socket、命名socket、监听socket、接受连接、发起连接、读写数据、获取地址信息、检测带外标记、读取和设置socket选项
- 网络信息API:实现主机名和IP地址的转换,端口号和服务名称的转换
5.1 socket地址API
5.1.1 主机字节序和网络字节序
- 字节序:对于32位机而言,CPU一次加载能装载4个字节,也就是32位数据,那么4个字节的数据在内存中的排列顺序就是字节序。
- 例如:01020304H,在内存中的排列数据有两种(顺序默认由低到高):04 03 02 01(小端序);01 02 03 04(大端序)
- 大端序:数据高位存储在内存低地址位
- 小端序:数据高位存储在内存高地址位
- 现代pc多用小段字节序,所以小端字节序又称为主机字节序
- 网络字节序也就是大端字节序。
-
主机字节序和网络字节序之间的转换
#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netlong);
5.1.2 通用socket地址
- socket网络编程接口中表示socket地址的结构体是sockaddr,定义如下
#include <bits/socket.h>
struct sockaddr{
sa_family_t sa_family; //地址族类型
char sa_data[14]; //存放socket地址值
}
- sa_family_t:地址族类型,与协议族类型对应
- sa_data用于存放socket地址值,
- 对于PF_UNIX和PF_INET6来说,16字节不够用,所以Linux定义了下边责怪新的通用socket地址结构体:
#include <bits/socket.h>
struct sockaddr_storage{
sa_family_t sa_family; //地址族类型
unsigned long int __ss_align;
char __ss_padding[128-sizeof(__ss_align)];
}
这个结构体是内存对齐的,也是就是[128-sizeof(__ss_align)]
的作用。编译器创建一个变量时候地址并不是随意取得,而是某一个数的倍数,这样就导致了有一部分内存是用不到的,而上述过程会把申请到的内存刚好用完,这就是内存对齐。
5.1.3 专用socket地址
- 通用地址获取IP地址和端口号需要额外操作,Linux提供专门的socket地址结构体
5.1.3.1 UNIX本地协议族专用socket地址结构体
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sin_family; //地址族
char sun_path[108]; //文件路径名
}
5.1.3.2 TCP/IP协议族专用socket地址结构体
IPv4
struct sockaddr_in {
sa_family_t sin_family; //地址族
u_int16_t sin_port; //端口号,网络字节序
struct in_addr sin_addr; //IPv4地址结构体
}
struct in_addr {
u_int32_t s_addr; //IPv4地址,用网络字节序表示
}
IPv6
struct sockaddr_in6 {
sa_family_t sin6_family; //地址族
u_int16_t sin6_port; //端口号,网络字节序
u_int32_t sint6_flowinfo; //流信息,应设置为0
struct in6_addr sin6_addr; //IPv6地址结构体
u_int32_t sin6_scope_id //scope ID, 处于实验阶段
}
struct in6_addr {
unsigned char sa_addr[16]; //IPv6地址,用网络字节序表示
}
所有专用socket地址以及sockaddr_storage类型的变量在实际使用的时候都需要强制转换
为通用的socket地址类型sockaddr。因为所有socket编程接口使用的地址参数类型都是sockaddr.
5.1.4 IP地址转换函数
- socket地址需要的IP地址都是二进制的,而日常用的都是点分的IP地址,所以需要转化。
- 转换函数
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr); //使用点十分进制字符串表示的IPv4地址转化为网络字节序表示的IPv4地址,失败返回INADDR_NONE
int inet_aton(const char* cp, struct in_addr* inp); //功能与inet_addr类似,但是会把结果存储在inp中,成功返回1,失败返回0
char * inet_ntoa(srtcut in_addr in); //把网络字节序的IPv4地址转化为用点分十进制表示的字符串。
注意inet_ntoa由于使用了一个静态变量,所以不可重入
- 同时适用IPv4和IPv6的转换函数
#include <arpa/inet.h>
int inet_pton(in af, const char* src, void* dst); //字符串表示的IP转化为网络字节序表示的IP,成功返回1,失败返回0
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt); //网络字节序IP到字符串表示的IP,成功返回目标存储单元的地址,失败返回NULL
- af指定地址族,AF_INET或AF_INET6
- src存储IP地址
- dst存储结果
- cnt指定目标存储单元大小,
INET_ADDRSTRLEN
用于IPv4,INET6_ADDRSTRLEN
用于IPv6
5.2 创建socket
- socket是可读、可写、可控制、可关闭的文件描述符。
- 创建socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol); //成功返回文件描述符,失败返回-1。
- domain参数告诉系统使用哪个底层协议族:PF_INET(IPv4)、PF_INET6(IPv6)、PF_UNIX(UNIX)
- type参数指定服务类型,SOCK_STREAM(流服务)、SOCK_UGRAM(数据报)。对于TCP/IP协议,前者代表TCP,后者代表UDP。
- type参数可以和
SOCK_NONBLOCK
和SOCK_CLOEXEC
参数相与(逻辑与)作为一个新参数,SOCK_NONBLOCK代表把新创建的socket设置为非阻塞,SOCK_CLOEXEC代表调用fork创建子进程时在子进程关闭该socket。 - protocol参数一般设置为0,代表默认情况。
5.3 命名socket
- 创建socket并没有指定地址,把socket和socket地址绑定的行为称为命名,也就是给他指定一个地址。
- 命名socket的系统调用是bind,定义如下:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen); //成功返回0,失败返回-1。并设置error
- sockfd是socket文件标识符。
- my_addr代表socket地址。
- addrlen表示该地址长度。
- ERROR常见是EACCES和EADDRINUSE,前者代表被绑定的地址是受保护的,也就是端口不能绑定。后者代表被绑定的地址正在使用中,例如绑定到了一个正在TIME_WAIT的socket。
5.4 监听socket
- 创建+命名socket之后,还不能马上接收连接,需要用socket文件描述符创建一个监听队列,用来存放待处理的客户连接。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- sockfd是指定监听的socket
- backlog是提示内核监听队列的最大长度,其值代表所有处于半连接和完全连接的socket的上限,在Linux2.2之后,只代表完全连接的socket的上限。backlog典型值为
5
。 - 编写一个服务器程序,设置backlog不同值,研究影响。建立连接的个数不一定是backlog,通过会略大。
//研究socket监听中block参数的影响
#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 <iostream>
static bool stop = false;
// sigterm信号处理函数,触发时结束主程序中的循环
static void handle_term(int sig) {
stop = true;
}
int main(int argc, char* argv[]) {
signal(SIGTERM, handle_term);
if (argc <= 3) {
std::cout << "usage: " << argv[0] <<" ip_address port_number backlog" << std::endl;
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int backlog = atoi(argv[3]);
//创建socket
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
//创建IPv4地址
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
assert(inet_aton(ip, &address.sin_addr) == 1);
//命名socket
assert(bind(sockfd, (struct sockaddr*)&address, sizeof(address)) != -1);
//监听
assert(listen(sockfd, backlog) != -1);
//循环等待
while(!stop){
sleep(1);
}
//关闭socket
close(sockfd);
return 0;
}
5.5 接受连接
- 从listen监听队列中接受一个连接
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //成功返回一个新连接的socket,失败返回-1。
-
sockfd是执行过listen系统调用的监听sockfd
-
addr用来获取被接受连接的远端socket地址
-
addrlen指定该socket地址长度
-
accpet只是负责从监听队列中取出连接,不论连接处于何种状态
5.6 发起连接
- 服务器端负责监听和接受连接,而客户端负责发起连接,也就是connect系统调用
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen); //成功返回0,失败返回-1。设置error
- serv_addr是服务器监听的socket地址
- addrlen为这个地址的长度。
- 常见的error:ECONNREFUSED,目标端口不存在,拒绝连接。ETIMEDOUT,连接超时。
5.7 关闭连接
- 关闭连接其实就是关闭对应的socket
#include <unistd.h>
int close(int fd);
-
close并不是立刻关闭连接,而是将fd的引用计数-1,当fd的引用计数为0时,才是真正关闭连接。
-
多进程中,一次fork系统调用默认将父进程打开的socket引用计数+1。因此必须对父进程和子进程的socket执行close调用才能将连接真正关闭。
-
如果必须立刻终止连接,而不是将引用计数-1,那就使用shutdown
#include <sys/socket.h>
int shutdown(int sockfd, in howto);
- howto决定了shutdown的行为
5.8 数据读写
5.8.1 TCP数据读写
- 对文件读写操作的read和write同样适用于socket。
- 专门用于TCP流数据读写的系统调用:
#include <sys/types.h>
#include <sys/socket.h>
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);
- recv负责读取sockfd中的数据,buf和len参数分别指定读缓冲区的位置和大小,flags参数通常为0。
- recv返回实际读取的数据长度,注意,这个长度不一定是我们希望的len,所以有可能需要多次recv读才能读取完整。
- send往sockfd写数据,buf和len指定写缓冲区的位置和大小。send成功时候返回实际写入的数据长度,失败返回-1。
- flags参数,可以取下表一个或几个的逻辑或。
- 使用MSG_OOB选项发送带外数据
定义一个类,用于简化sockfd的创建,和连接
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
//创建IPv4
class createSockfd{
struct sockaddr_in address;
public:
int sockfd;
struct sockaddr_in client;
socklen_t client_addrlength;
int connfd; //accept接受到的socketfd
public:
createSockfd(const char *ip, int port){
sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
assert(inet_aton(ip, &address.sin_addr) == 1);
client_addrlength = sizeof(client);
}
~createSockfd(){
close(sockfd);
close(connfd);
}
int bindSockfd();
int connectSockfd();
int listenfd(int backlog = 5);
int acceptfd();
};
//命名socket
int createSockfd::bindSockfd(){
return bind(sockfd, (struct sockaddr*)&address, sizeof(address));
}
//连接socket
int createSockfd::connectSockfd(){
return connect(sockfd, (struct sockaddr*)&address, sizeof(address));
}
//监听socket
int createSockfd::listenfd(int backlog){
return listen(sockfd, backlog);
}
int createSockfd::acceptfd(){
connfd = accept(sockfd, (struct sockaddr*)&client, &client_addrlength);
return connfd;
}
客户端,用于发送报文,这里一共发送了三个报文,“123” “abc” “123”,其中“abc”报文被认为是带外数据。
#include "../create_sockfd.h"
#include <iostream>
#include <stdlib.h>
int main(int argc, char *argv[]){
if (argc <=2){
std::cout << "usage: " << basename(argv[0]) << " ip_address port_number" << std::endl;
return 1;
}
createSockfd sockfd(argv[1], atoi(argv[2]));
if (sockfd.connectSockfd() < 0){
std::cout << "connect faild." << std::endl;
} else {
const char* oob_data = "abc";
const char* normal_data = "123";
send(sockfd.sockfd, normal_data, strlen(normal_data), 0);
send(sockfd.sockfd, oob_data, strlen(oob_data), MSG_OOB);
send(sockfd.sockfd, normal_data, strlen(normal_data), 0);
}
return 0;
}
服务器端报文,用于接收数据
#include "../create_sockfd.h"
#include "iostream"
#include <stdlib.h>
#include <errno.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[]){
if(argc <= 2){
std::cout << "usage: " << basename(argv[0]) << " ip_address port_number" << std::endl;
return 1;
}
createSockfd socketfd(argv[1], atoi(argv[2]));
socketfd.bindSockfd();
socketfd.listenfd(5);
socketfd.acceptfd();
if(socketfd.connfd < 0){
std::cout << "error : " << errno << std::endl;
} else {
char buffer[BUF_SIZE];
memset(buffer, '\0', BUF_SIZE);
int ret = recv(socketfd.connfd, buffer, BUF_SIZE - 1, 0);
std::cout << "got " << ret << " bytes of normal data '" << buffer << "'" << std::endl;
memset(buffer, '\0', BUF_SIZE);
ret = recv(socketfd.connfd, buffer, BUF_SIZE - 1, MSG_OOB);
std::cout << "got " << ret << " bytes of odd data '" << buffer << "'" << std::endl;
memset(buffer, '\0', BUF_SIZE);
ret = recv(socketfd.connfd, buffer, BUF_SIZE - 1, 0);
std::cout << "got " << ret << " bytes of normal data '" << buffer << "'" << std::endl;
}
return 0;
}
接收到
got 1 bytes of odd data 'c'
got 5 bytes of normal data '123ab'
got 3 bytes of normal data '123'
5.8.2 UDP数据的读写
- 用于UDP数据报读写的系统调用是:
#include <sys/typpes.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_adr, socklen_t* addrlen);
ssize_ sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
-
recvfrom读取sockfd上的数据,buf和len分别是指定缓冲区的位置和大小。因为UDF不连接,所以每一次都得传递地址,addrlen是地址的长度。
-
sendto往sockfd写数据,buf和len是指定缓冲区的位置和大小,dest_addr是指定地址的位置,addrlen是指定地址的大小。
-
recvfrom和sendto也可以用于面向连接的socket,只需要把后边的参数和职位NULL,因为此时的地址已经知道了。
5.8.3 通用数据读写函数
- 可以同时用于TCP流数据和UDP数据报。
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
struct msghdr {
void* msg_name; //socket地址
socklen_t msg_namelen; //socket地址长度
struct iovec* msg_iov; //分散的内存块
int msg_iovlen; //分散的内存块数量
void* msg_control; //指向辅助数据的起始位置
socklen_t msg_controllen; //辅助数据的大小
int msg_flagsl //辅助函数中的flags参数,会在调用中更新。
};
struct iovec{
void *iov_base; //内存块起始地址
size_t iov_len; //内存块长度
};
- 对于TCP, msg_name无意义且必须被设置为NULL。
- msg_iov是iovec结构体的指针,msg_iovlen说明了这些内存块有几个。
- 对于sendmsg而言,内存块中的数据会被一并发送,这是集中写
- 对于recvmsg而言,数据会被读取并存放在msg_iovlen块分散的内存中,这被称为分散读。
- 辅助数据略。
- flags参数与send/recv的flags参数含义相同。
5.9 带外标记
- 准确接收带外数据,需要让应用程序得到通知,知道有带外数据需要接收,还需要知道带外数据在数据流中的具体位置。
- 系统调用:
#include <sys/socket.h>
int sockatmark(int sockfd);
- sockatmark会判断sockfd是否处于带外标记,也就是下一个被读取到的数据是不是带外数据,如果是,返回1,我们就可以用带MSG_OBB标记的recv调用来接收带外数据,不是就返回0。
5.10 地址信息函数
- 从一个连接的socket知道本端以及远端的socket地址。
#inlcude <sys/socket.h>
//成功返回0,失败返回-1。
int getsocketname(int sockfd, struct sockaddr* address, socklen_t* address_len); //获取本端,存储在address中
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len); //获取远端
5.11 socket选项
- socket是一个文件描述符,那就可以被设置属性。
#include <sys/socket.h>
//成功返回0,失败返回-1
//读取socket文件描述符
int getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
//设置socket文件描述符
int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len);
- level代表要操作的哪个协议
- option_name指定选项名字
- option_value和option_len被操作选项的值和长度,0代表禁止,非零代表启用
几个比较重要的选项
5.11.1 SO_REUSEADDR
- 设置了该选项的socket会强制使用因为处于TIME_WAIT状态而被占用的地址。
5.11.2 SO_RCVBUF和SO_SNDBUF
- SO_RCVBUF:设置TCP接收缓冲区
- SO_SNDBUF:设置TCP发送缓冲区
- 就算设置了值,系统也会加倍,而且不得小于某个最小值,例如TCP接收缓冲区的最小字节是256,发送缓冲区的最小字节是2048。不同系统可能有不同的值。
- 之所以这么设置,主要是为了有足有的空闲缓冲区来处理拥塞。
- 可以做一下实验,先设置TCP接收缓冲区,再读取他
...
int recvbuf = 50;
int len = sizeof(recvbuf);
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof(recvbuf));
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recvbuf, (socklen_t*)&len);
std::cout << "Tcp recevie buffer size after setting is " << recvbuf << std::endl;
...
5.11.3 SO_RCVLOWAT和SO_SNDLOWAT
- SO_RCVLOWAT:TCP接收缓冲区的低水位标记
- SO_SNDLOWAT:TCP发送缓冲区的低水位标记
- 用来判断socket是否可读或可写,当TCP的接收缓冲区中的可读数据总数大于低水位标记时,IO复用通知应用程序可以从相应的socket上读取数据。同理,当TCP的发送缓冲区空闲空间大于低水位标记,IO复用通知应用程序可以往socket写入数据。
- 默认情况下,低水位标记均为1字节。
5.11.4 SO_LINGER
- 控制close系统调用关闭TCP连接时的行为。
- 默认关闭TCP时,把TCP发送缓冲区的残余数据发送过去。
- 设置SO_LINGER需要传递一个linger类型的结构体。
#incldue <sys/socket.h>
struct linger {
int l_onoffl; //开启(非0),关闭(0)
int l_inger; //滞留时间
}
略
5.12 网络信息API
- 通过主机名称或IP地址获取主机完整信息。
5.12.1 gethostbyname和gethostbyaddr
- gethostbyname通过主机名称获取主机信息
- gethostbyaddr通过IP地址获取主机信息
#include <netdb.h>
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
struct hostent {
char* h_name; //主机名称
char** h_aliases; //主机别名列表
int h_addrtype; //地址类型
int h_length; //地址长度
char** h_addr_list; //网络字节序,主机IP地址列表。
};
- name:目标主机名称
- addr:目标主机IP地址;len:IP地址长度;type:IP地址类型,AF_INET和AF_INET6
5.12.2 getservbyname和getservbyport
- getservbyname:通过服务名称获取服务信息
- getservbyport:通过端口获取服务信息
#include <netdb.h>
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
struct servent{
char* s_name; //服务名称
char** s_aliases; //服务别名
int s_port; //端口号
char* s_proto; //服务类型,通常是tcp或udp
};
- proto参数指定服务类型,tcp表示获取流服务,udp表示获取数据包服务,NULL表示获取所有类型的服务。
上边四个函数都是不可重入的,要用可重入版本在原函数名尾部加上 _r
5.12.3 getaddrinfo
略
5.12.4 getnameinfo
略