文章目录
总览
大端小端
大端字节序: 又称为网络字节序。大端字节序一般指:一个整数的高位字节放在地址的低位地址上。(Java也是大端)(大端法与一般人类的写法习惯相同)
小端字节序: 又称主机字节序。 一般指整数的高位字节位于地址的高位地址上。
Cpp中一般可以使用union判断大端小端,利用了union的特性:从低位地址向高位地址延伸。
大小端检测
union{
short val;
char bytes[sizeof(short)];
} test;
void check(){
test.val = 0x0102;
if(test.bytes[0] == 1 && test.bytes[1] == 2){
// 大端
}else if(test.bytes[0] == 2 && test.bytes[1] == 1){
// 小端
}else{
// What the f***?
}
}
大小端转换
在<netinet/in.h>中提供了转换的函数
#include<netinet/in.h>
// 一般用来转ip
// 将32位的网络字节序(大端)转化位主机字节序(小端)
unsigned long int ntohl(unsigned long int netlong);
// 将32位的主机字节序转为网络字节序
unsigned long int htonl(unsigned long int hostlong);
// 一般用来转port
unsigned short int htons(unsigned short int hostshort);
unsigned short int ntohs(unsigned short int netshort);
socket地址API
通用socket地址
socket网络编程中的地址是结构体sockaddr,在头文件bits/socket.h
中
struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
};
sa_family
是地址族类型sa_family_t
的变量,常见的协议族(protocol family,又称domain)有
- PF_UNIX (等价AF_UNIX) , UNIX本地域协议族
- PF_INET (AF_INET), TCP/IPv4协议族
- PF_INET6 (AF_INET6), TCP/IPv6协议族
其中PF_* 与 AF_*等价。
sa_data存放相应的socket地址值
- PF_UNIX, 文件路径名,可达108字节
- PF_INET,16bit的端口号,32bit的IPv4地址,共6B
- PF_INET6, 16bit端口号,32bit流标识,128bit IPv6地址,32bit范围id
专用socket地址
PF_UNIX专用地址
#include<sys/un.h>
struct sockaddr_un{
sa_family_t sin_family; // protocol family
char sun_path[108];// file path name
};
PF_INET
与scokaddr的区别是,没有将端口号和IP地址粘连起来。
#include<netinet/in.h>
struct sockaddr_in{
sa_family_t sin_family;
u_int16_t sin_port; // port
struct in_addr sin_addr; // Ipv4 struct
};
strcut in_addr{
u_int32_t s_addr; // ipv4 address, 用网络字节序(大端)表示
};
PF_INET6
用于IPv6,需要的时候查就可以了,不需要特意记忆
总结
这些专用结构解决了sockaddr将port与address粘连在一个array的问题。
**Key:**所有的专用socket地址类型的变量在实际使用的时候都需要转化为通用的socket地址类型sockaddr(可以直接强制转化)。因为所有socket接口的地址参数都是默认sockaddr。
IP转换函数
include<arpa/inet.h>
// 将字符串IPv4转为网络字节序的IPv4
in_addr_t inet_addr(const char* strptr);
// 同上,将结果存在inp指针内,成功返回1
int inet_aton(const char* cp, struct in_addr* inp);
// 将网络字节序整数表示的IPv4转化为字符串
char* inet_ntoa(struct in_addr in);
Notice: inet_ntoa
内部使用静态变量存储转化结果,因此是不可重入的。
例子
#include<iostream>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
int main(){
const char* strIP = "1.13.2.13";
in_addr ip;
inet_aton(strIP, &ip);
const char* ipadd = inet_ntoa(ip);
cout<<"strIP is "<<strIP<<", in_addr_t is "<<ip.s_addr<<", to ip address is "<<ipadd<<endl;
cout<<"in_addr_t is "<<inet_addr(strIP)<<endl;
in_addr ip2;
ip2.s_addr = inet_addr("192.168.0.1");
const char* ip2add = inet_ntoa(ip2);
cout<<"ip1 address is "<<ipadd<<endl;
cout<<"ip2 address is "<<ip2add<<endl;
}
/*
console is:
strIP is 1.13.2.13, in_addr_t is 218238209, to ip address is 1.13.2.13
in_addr_t is 218238209
// 说明了不可重载性
ip1 address is 192.168.0.1
ip2 address is 192.168.0.1
/*
也可以使用inet_pton
进行转换
int inet_pton(int af, const char* src, void* dst)
int main(){
in_addr IP;
cout<<inet_pton(PF_INET, "192.168.0.0", &IP)<<endl;
cout<<IP.s_addr<<" "<<inet_addr("192.168.0.0")<<endl;
cout<<ntohl(inet_addr("192.168.0.0"))<<endl;
}
/*
1
// 默认转换的都是网络序
43200 43200
// 可以使用ntols转换为主机序
3232235520
*/
使用socket
创建socket
socket在Linux中也是一个文件。返回一个文件描述符,失败返回-1
int socket(int domain, int type, int protocol)
domain
使用的协议栈, IPv4——PF_INET, UNIX本地域——PF_UNIX
type
SOCK_STREAM 流服务,对TCP/IP domain来说就是TCP
SOCK_UGRAM 数据报服务,对TCP/IP domain来说就是UDP
protocol
几乎所有的默认协议族默认设为0就ok,特殊情况查文档
命名socket——bind()
将socket绑定到ip和port上
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen)
将my_addr绑定到sockfd的socket上, addrlen指出了该socket地址的长度。bind成功返回0,失败返回-1并设置errno,常见的errno:
- EACCES:被绑的的地址是受保护地址(0-1023)
- EADDRINUSE,被绑定地址在使用中。比如绑定到了一个在TIME_WAIT状态的socket地址。
监听socket——listen
创建一个监听队列以存放待处理的客户链接
int listen(int sockfd, int backlog)
sockfd指定被监听的socket,backlog指定内核监听队列的最大长度。如果监听队列的长度超过backlog的话,将不受理新的连接。成功返回0,失败-1
Linux2.2之前,backlog指的是SYN_RCVD状态和ESTABLISHED状态的个数,2.2后指的是ESTABLISHED状态的个数。 半连接状态的上限在内核参数中设置。
发起连接——connect
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen)
成功时返回0,一旦成功,sockfd就唯一标识了这个连接。客户端就可以通过读写sockfd来与服务器通信。
关闭连接——close
直接使用关闭文件的系统调用来完成。
unistd
中的int close(int fd)
。
调用之后将fd的引用计数减一,当fd引用计数为0之后就真正关闭连接。fork操作会使父进程的fd加一,因此只有父子进程都执行了close才会关闭连接。
强制关闭,不管减一
int shutdown(int fd, int howto)
howto: SHUT_RD, SHUT_WR, SHUTRDWR 分别为关闭读、写,读写。
数据读写
可以使用基于文件的write,read。
TCP数据读写
也有专用的系统调用接口,TCP的是
#include<sys/socket.h>
#include<sys/types.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);
buf和len指定缓冲区和大小。flags一般设置为0即可。
recv成功时返回读取到数据的长度,可能小于len,所以有时需要多次调用recv才可以成功收集到数据。返回0代表对方已经关闭了连接。
send是往sockfd写入数据,buf和len指定写缓冲区和大小。成功时返回实际写入数据的长度。
flags:
UDP数据读写
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, int len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
Example
最基本的服务端和客户端模型。
注意:如果用的云服务器,一般服务商是默认关闭端口的,要在防火墙手动开启。
Server
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdio.h>
#include<signal.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<cstring>
#include<assert.h>
#include<iostream>
const int BUF_SIZE = 1024;
int main() {
int port = 19980;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
address.sin_addr.s_addr = htonl(INADDR_ANY);
int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock > 0);
int ret = bind(sock, (sockaddr*)(&address), sizeof(address));
perror("bind");
assert(ret != -1);
// 5个最大连接数
ret = listen(sock, 5);
perror("bind");
assert(ret != -1);
sockaddr_in client;
socklen_t client_len = sizeof(client);
printf("Start accept\n");
int connfd = accept(sock, (sockaddr*)(&client), &client_len);
printf("Accepted\n");
if (connfd < 0) {
std::cout << "error " << errno << std::endl;
}
else {
printf("Do\n");
char buffer[BUF_SIZE];
memset(buffer, '\0', BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE - 1, MSG_OOB);
std::cout << "Get " << ret << " bytes, is " << buffer << std::endl;
close(connfd);
}
printf("Ready to close");
getchar();
close(sock);
return 0;
}
Client
int main() {
const char* ip = "Your IP address";
int port = 19980;
sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);
int sock = socket(AF_INET, SOCK_STREAM, 0);
perror("socket");
assert(sock >= 0);
int ret = connect(sock, (sockaddr*)&address, sizeof(address));
perror("connect");
if(ret < 0){
std::cout<<"connect fail"<<std::endl;
}else{
const char* data = "abc";
send(sock, data, strlen(data), 0);
std::cout<<"OK"<<std::endl;
}
close(sock);
return 0;
}
通用数据读写函数
#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 address
socklen_t msg_namelen; // length of socket address
struct iovec* msg_iov; // 分散的内存块
int msg_iovlen; // 分散内存块的数量
void* msg_control; // 辅助数据的起始位置
socklen_t msg_controllen; // 辅助数据的大小
int msg_flags; // 复制函数中的flags参数,并在调用过程中更新
};