Linux网络编程基础API
主机字节序和网络字节序
现代CPU的累加器一次都能装载(至少)4字节(这里考虑32位机),即一个整数.那么这4字节在内存中排列的顺序将影响它被累加器装载成的整数的值.
1. 字节序的分类
-
大端字节序(big endian)
大端字节序是指一个整数的高位字节(23-31bit)存储在内存的低地址处,低位字节(0~7bit)存储在内存的高地址处.
-
小端字节序(little endian)
小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处.
2. 主机字节序和网络字节序
- 现代PC大多采用小端字节序,因此小端字节序又被称为网络字节序
- 现在网络传输中的字节序都采用大端字节序,所以大端字节序又被称为网络字节序
3. 字节序转换的作用
解决不同主机不同字节序间数据传输的问题. 发送数据时将本机字节序转化为网络字节序,接收数据时将网络字节序转化为主机字节序.
4. 字节序之间的转化函数
#include<netinet/in.h>
//host to network long 将长整形(32bit)的主机字节序数据转化为网络字节序
unsigned long int htonl(unsigned long int hostlong);
//host to network short 将短整形(16bit)的主机字节序数据转化为网络字节序
unsigned short int htons(unsigned short int hostshort);
//network to host long 将长整形(32bit)的网络字节序数据转化为主机字节序
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
5. 判断机器字节序
#include<cstdio>
void byteorder(){
union{
short value;
char union_bytes[sizeof(short)];
}test;
test.value = 0x0102;
if((test.union_bytes[0] == 1) && (test.union_bytes[1]) == 2){
puts("big endian\n");
}
else if((test.union_bytes[0] == 2) && (test.union_bytes[1]) == 1){
puts("little endian\n");
}
else{
puts("unknown...\n");
}
}
int main(){
byteorder();
}
socket地址
1. 通用socket地址
sockaddr
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
}
-
sa_family
成员是地址族类型(sa_family_t
)的变量,地址族类型通常与协议族类型对应-
常见的协议族
协议族 地址值含义和长度 地址族 描述 PF_UNIX 文件的路径名,长度可达到108字节 AF_UNIX UNIX本地域协议族 PF_INET 16bit端口号和32bitIPv4地址,共5字节 AF_INET TCP/IPv4协议族 PF_INET6 16bit端口号,32bit流标识,126bitIPv6地址,32bit范围ID,共26字节 AF_INET6 TCP/IPv6协议族 - 宏
PF_*
和AF_*
都定义在bits/socket.h
头文件中,且后者与前者具有完全相同的值,所以二者通常混用
- 宏
-
-
sa_data
成员用于存放socket地址值. 由上表可见,14字节的sa_data
根本无法完全容纳多数协议族的地址值,因此Linux定义了下面这个通用的socket地址结构体
sockaddr_storage
#include<bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[128-sizeof(__ss_align)];
}
// 不仅提供了更大的空间用于存放地址值,而且是内存对齐的(__ss_align)
2. 专用socket地址
sockaddr_un
#include<sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family; // 地址族:AF_UNIX
char sun_path[108]; // 文件路径名
};
sockaddr_in
#include<sys/socket.h>
struct sockaddr_in
{
sa_family_t sin_family; // 地址族:AF_INET
u_int16_t sin_port; //端口号,要使用网络字节序表示
struct in_addr sin_addr; //IPv4地址结构第
char sin_zero[8]; //不使用
}
struct in_addr
{
u_int32_t s_addr; //IPv4地址,要用网络字节序表示
}
//sockaddr_in结构体的赋值
struct sockaddr_in addr;
char * serv_port = "9999";
char * serv_ip = "127.0.0.1";
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET; //设置地址族
// addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置ip地址,服务器通常采用INADDR_ANY
addr.sin_addr.s_addr = inet_addr(serv_ip); //客户端
addr.sin_port = htons(atoi(serv_port)); // 设置端口
//INADDR_ANY
//自动分配服务器端的IP,不必输入,服务器有限考虑这种方式
-
通用
sockaddr
不好用,比如设置与获取IP地址和端口号就需要执行繁琐的位操作,所以使用sockaddr_in
进行赋值,再转化为sockaddr
struct sockaddr_in addr; ... if(bind(serv_sock,(struct sockaddr*)&addr),sizeof(addr) == -1); error_handling("bind() error"); ...
sockaddr_in6
#include<sys/socket.h>
struct sockaddr_in6
{
sa_family_t sin5_family; //地址族,要用AF_INET6
u_int16_t sin6_port; //端口号,使用网络字节序
u_int32_t sin6_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地址,网络字节序表示
}
3. IP地址转换函数
#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);
int inet_pton(int af,const char * src,void * dst);
const char * inet_ntop(int af,const void * src,char * dst,socklen_t cnt);
inet_addr
inet_addr
将点分十进制字符串标示的IPv4地址转化为用网络字节序整数表示的IPv4地址.失败时返回INADDR_NONE.
inet_aton
inet_aton
将点分十进制字符串标示的IPv4地址转化为用网络字节序整数表示的IPv4地址存储到参数inp指向的地址结构中.成功时返回1,失败时返回0
inet_ntoa
inet_ntoa
将用网络字节序整数标示的IPv4地址转化为用点分十进制字符串标示的IPv4地址. 但inet_ntoa
函数是不可重入的,该函数内部使用一个静态变量存储转化结果,函数的返回值指向该静态内存
#include<arpa/inet.h>
...
struct sockaddr_in addr1,addr2;
addr1.sin_addr.s_addr = htonl(0x1020304);
addr2.sin_addr.s_addr = htonl(0x1010101);
char * str_ptr1,str_ptr2;
str_ptr1 = inet_ntoa(addr1.sin_addr); //str_ptr1: 1.2.3.4
//str_ptr2 = inet_ntoa(addr2.sin_addr); //str_ptr1: 1.1.1.1, str_ptr2: 1.1.1.1
//第二次调用修改了函数中的静态变量
//为了防止之前的字符串被修改,应该将之前的字符串保存到其他内存空间
char str_arr[20];
strcpy(str_arr,str_ptr1); // str_arr: 1.2.3.4
//str_ptr2 = inet_ntoa(addr2.sin_addr); //str_ptr2: 1.1.1.1, str_arr:1.2.3.4
inet_pton
-
inet_pton
函数将用字符串表示的IP地址src(用点分十进制字符串标示的IPv4地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序表示的IP地址,并把转换结果存储与dst指向的内存中. -
af
:指定地址族,可以是AF_INET或者AF_INET6 -
成功返回1,失败返回0并设置errno
inet_ntop
-
inet_ntop
函数进行和inet_pton
相反的转换.前三个参数的含义与inet_pton
相同 -
cnt
:制定目标存储单元的大小,使用下面两个宏#include<netinet/in.h> #define INET_ADDRSTRLEN 16 #define INET6_ADDRSTRLEN 46
-
成功时返回目标存储单元的地址,失败返回NULL并设置errno
函数使用
/*
* @Descripttion:
* @Version:
* @Author: zsj
* @Date: 2020-05-03 09:59:49
* @LastEditors: zsj
* @LastEditTime: 2020-05-03 13:48:51
*/
#include<sys/socket.h>
#include<unistd.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<cstring>
#include<cstdio>
#include<netinet/in.h>
int main()
{
sockaddr_in addr;
addr.sin_addr.s_addr = htonl(0x1020304);
char * str_addr = inet_ntoa(addr.sin_addr);
printf("%s\n",str_addr); // 1.2.3.4
in_addr_t addr_t = inet_addr(str_addr);
printf("%X\n",ntohl(addr_t)); //1020304
sockaddr_in temp;
inet_aton(str_addr,&temp.sin_addr);
printf("%X\n",ntohl(temp.sin_addr.s_addr)); //1020304
sockaddr_in temp2;
inet_pton(AF_INET,str_addr,(void*)&temp2.sin_addr.s_addr);
printf("%X\n",ntohl(temp2.sin_addr.s_addr)); //1020304
// char str_arr[INET_ADDRSTRLEN];
// char * adr = inet_ntop(AF_INET,&addr.sin_addr,const_cast<char*>(str_arr),sizeof(str_arr));
}
socket核心函数
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
int socket(int domain,int type,int protocl);
int bind(int sockfd,const struct sockaddr*my_addr,socklen_t addrlen);
int listen(int sockfd,int backlog);
int accept(int sockfd,struct sockaddr*addr,socklen_t*addlen);
int connect(int sockfd,const struct sockaddr*serv_addr,socklen_t addrlen);
int close(int fd);
1. 服务器端
socket
创建一个socket.
domain
:使用的是哪个底层协议族,有三个值- PF_INET, IPv4
- PF_INET6, IPv6
- PF_UNIX, UNIX本地协议族
type
:指定服务类型,- SOCK_STREAM,流服务,对应TCP协议
- SOCK_DGRAM,数据报服务,对应UDP协议
- SOCK_NONBLOCK ,与上述1,2个值相与,表示将新创建的socket设置为非阻塞的(Linux内核版本>2.6.17)
- SOCK_CLOEXEC,与上述1,2个值相与,表示当用fork创建子进程时在子进程中关闭该socket(Linux内核版本>2.6.17)
protocol
:在前两个参数构成的协议集合下,再选择一个具体的协议.在多数情况下,都应该设置为0- 函数成功时返回一个socket描述符,失败则返回-1,并设置errno
bind
命名socket. 将一个socket与socket地址绑定称为给socket命名.
sockfd
:要分配地址信息的套接字文件描述符my_addr
:存有地址信息的结构体变量地址值addrlen
:第二个结构体变量的长度- 成功返回0,失败返回-1并设置errno,其中常见的两种errno是:
- EACCES:被绑定的地址是收保护的地址,仅超级用户能够访问
- EADDRINUSE:被绑定的地址正在使用中.
listen
socket被命名之后不能马上接受客户连接,需要使用listen系统调用来创建一个监听队列以存放待处理的客户.
sockfd
:制定被监听的socketbacklog
:提示内核监听队列的最大长度,典型值是5- 成功时返回0,失败时返回-1并设置errno
accept
从listen监听队列中接收一个连接.调用此函数后会阻塞
sockfd
:是执行过listen系统调用的监听socketaddr
:用来获取被接受连接的远端socket地址addrlen
:指出addr
接收的socket地址的长度- 成功时返回一个新的连接socket,失败时返回-1并设置errno
close
关闭一个socket连接
fd
:要关闭的socket的描述符- close系统调用并不是立即关闭一个连接,而是将fd的引用计数减一.只有当fd的引用计数为0时,才真正的关闭连接.在多进程程序中fork()默认使父进程中打开的socket引用计数加1,只有父子进程中都对该socket执行close才能完全关闭
2. 客户端
socket
connect
主动与服务器建立连接.调用函数后会阻塞
-
sockfd
:客户端套接字文件描述符 -
serv_addr
:保存目标服务端地址信息的变量地址值 -
addrlen
:以字节为单位传递以传递给第二个结构体参数serv_addr的变量地址的长度 -
成功时返回0,失败返回-1并设置errno,如下两种errno:
- ECONNREFUSED:目标端口不存在,连接被拒绝
- ETIMEDOUT:连接超时.
close
3.数据读写
TCP数据读写
#include<sys/types.h>
#include<sys/socket.h>
ssize_t read (int fd, void *buf, size_t nbytes);
ssize_t recv(int sockfd,void*buf,size_t len,int flags);
ssize_t write(int fd, const void *buf, size_t n);
ssize_t send(int sockfd,const void * buf,size_t len,int flags);
read
接受或者输入数据
fd
:显示数据接受对象的文件描述符buf
:要保存接收数据的缓冲区地址nbytes
:要接收数据的最大字节数- 成功时返回接收到的字节数,接收到文件尾或者连接被关闭返回0,失败返回-1
write
向文件或者套接字传递数据.
fd
:显示数据传输对象的文件描述符buf
:保存要传输数据的缓冲地址值n
:要传输数据的字节数- 成功时返回写入的字节数,失败时返回-1
recv
接受或者输入数据
sockfd
:表示数据接收对象的连接的套接字文件描述符buf
:保存接收数据的缓冲地址值len
:可接收的最大字节数flags
传输数据时制定的可选项信息- 成功时返回接受的字节数(收到EOF返回0),失败时返回-1;
send
向文件或者套接字传递数据
sockfd
:表示与数据传输对象的连接的套接字文件描述符buf
:保存带传输数据的缓冲地址值len
:待传输的字节数flags
传输数据时制定的可选项信息- 成功时返回发送的字节数,失败时返回-1
UDP数据读写
#include<sys/socket.h>
ssize_t sendto(int sock,void * buff,size_t nbytes,int flags,struct sockaddr* to,socklen_t addrlen);
ssize_t recvfrom(int sock,void * buff,size_t nbytes,int flags,struct sockaddr*from.socklen_t * addrlen);
sendto
sock
:用于传输数据的UDP套接字文件描述符buff
:保存待传输数据的缓冲区地址值nbytes
:待传输的数据长度,以字节为单位flags
:可选项参数,若没有则传递0to
:存有目标地址信息的sockaddr结构体变量的地址值addrlen
:传递给参数偷得地址值结构体变量长度- 成功时返回传输的字节数,失败时返回-1
recvfrom
sock
:用于接收数据的UDP套接字文件描述符buff
:保存接收数据的缓冲区地址值nbytes
:可接受的最大字节数,故无法超过参数buff所指的缓冲区大小flags
:可选项参数,若没有则传入0from
:存有发送端地址信息的sockaddr结构体变量的地址值addrlen
:保存参数from的结构体变量长度的变量地址值
socket编程实践(1)
TCP
服务器端
/**
* @File:serv_1.cpp
* @Descripttion: socket编程服务器端,简单版本(TCP)
* 只接受一个客户端连接,并接收客户端发送的信息显示后就退出
* @Version:
* @Author: zsj
* @Date: 2020-05-03 15:38:31
* @LastEditors: zsj
* @LastEditTime: 2020-05-03 16:20:39
*/
#include<cstdio>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<cstring>
void show_error(char * info){
printf(info);
exit(-1);
}
int main(int argc,char * argv[])
{
//参数数量不对直接返回
if(argc < 2){
printf("Usage: %s <port>\n",argv[0]);
exit(-1);
}
// 创建一个socket
int listenfd = socket(PF_INET,SOCK_STREAM,0);
if(listenfd == -1){
show_error("socket() error!");
}
// 设置socket地址
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[1]));
//分配地址信息,socket命名
int ret = bind(listenfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret == -1){
show_error("bind() error!");
}
//创建一个监听队列
ret = listen(listenfd,5);
if(ret == -1){
show_error("listen() error!");
}
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_sz = sizeof(clnt_addr);
//取出一个连接
int clnt_sock = accept(listenfd,(struct sockaddr*)&clnt_addr,&clnt_addr_sz);
if(clnt_sock == -1){
show_error("accept() error!");
}
else{
printf("get a connection %d\n",clnt_sock);
}
char buffer[128];
//读取数据
ret = read(clnt_sock,buffer,sizeof(buffer));
if(ret < 0){
show_error("read() error!");
}
else if(ret == 0){
printf("client close the connection!");
}
else{
printf("from %d data %d bytes: %s\n",clnt_sock,ret,buffer);
}
close(clnt_sock);
close(listenfd);
}
客户端
/**
* @File:clnt_1.cpp
* @Descripttion: socket编程,客户端简单版本,(TCP)
* 连接服务器之后,发送数据到服务器之后就退出
* @Version:
* @Author: zsj
* @Date: 2020-05-03 16:06:36
* @LastEditors: zsj
* @LastEditTime: 2020-05-03 16:19:46
*/
#include<cstdio>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<cstring>
void show_error(char * info){
printf(info);
exit(-1);
}
int main(int argc,char * argv[])
{
if(argc < 3){
printf("Usage: %s <ip> <port>\n",argv[0]);
exit(-1);
}
//创建一个socket
int clnt_sock = socket(PF_INET,SOCK_STREAM,0);
if(clnt_sock == -1){
show_error("socket() error!");
}
//客户端不需要绑定和监听,直接连接服务器
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_port = htons(atoi(argv[2]));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(clnt_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret == -1){
show_error("connect() error!");
}
else{
printf("connect to %s success!\n",argv[1]);
}
//发送数据
char buffer[128] = "hello server!!!";
ret = write(clnt_sock,buffer,sizeof(buffer));
if(ret < 0){
show_error("write() error!");
}
else{
printf("send data success!\n");
}
//关闭连接
close(clnt_sock);
}
UDP
服务器端
/*
* @File: serv_udp_1.cpp
* @Descripttion: socket编程服务器端,简单版本(UDP)
* 只接受一个客户端连接,并接收客户端发送的信息显示后就退出
* @Version:
* @Author: zsj
* @Date: 2020-05-03 17:05:25
* @LastEditors: zsj
* @LastEditTime: 2020-05-03 17:14:33
*/
#include<cstdio>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<cstring>
void show_error(char * info){
printf(info);
exit(-1);
}
int main(int argc,char * argv[])
{
//参数数量不对直接返回
if(argc < 2){
printf("Usage: %s <port>\n",argv[0]);
exit(-1);
}
// 创建一个socket
int serv_sock = socket(PF_INET,SOCK_DGRAM,0);
if(serv_sock == -1){
show_error("socket() error!");
}
// 设置socket地址
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[1]));
//分配地址信息,socket命名
int ret = bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret == -1){
show_error("bind() error!");
}
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_sz = sizeof(clnt_addr);
char buffer[128];
ret = recvfrom(serv_sock,buffer,sizeof(buffer),0,(sockaddr*)&clnt_addr,&clnt_addr_sz);
if(ret < 0){
show_error("read() error!");
}
else if(ret == 0){
printf("client close the connection!");
}
else{
printf("from %s data %d bytes: %s\n",inet_ntoa(clnt_addr.sin_addr),ret,buffer);
}
close(serv_sock);
}
客户端
/*
* @File:clnt_udp_1.cpp
* @Descripttion: socket编程,客户端简单版本(UDP)
* 连接服务器之后,发送数据到服务器之后就退出
* @Version:
* @Author: zsj
* @Date: 2020-05-03 16:06:36
* @LastEditors: zsj
* @LastEditTime: 2020-05-03 17:16:48
*/
#include<cstdio>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<cstring>
void show_error(char * info){
printf(info);
exit(-1);
}
int main(int argc,char * argv[])
{
if(argc < 3){
printf("Usage: %s <ip> <port>\n",argv[0]);
exit(-1);
}
//创建一个socket
int clnt_sock = socket(PF_INET,SOCK_DGRAM,0);
if(clnt_sock == -1){
show_error("socket() error!");
}
//客户端不需要绑定和监听,直接连接服务器
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_port = htons(atoi(argv[2]));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
//发送数据
char buffer[128] = "hello server!!!";
int ret = sendto(clnt_sock,buffer,sizeof(buffer),0,(sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret < 0){
show_error("sendto() error!");
}
else{
printf("send data success!\n");
}
//关闭连接
close(clnt_sock);
}