网络字节序
网络字节序是指多字节变量在网络传输时的表示方法,它采用大端字节序的表示方式。大端字节序是指将高序字节存储在起始地址,而低序字节存储在后续地址。
网络字节序和主机字节序的转化
- htonl(host to network long):将unsigned long类型的主机字节序转换为网络字节序。
- htons(host to network short):将unsigned short类型的主机字节序转换为网络字节序。
- ntohl(network to host long):将unsigned long类型的网络字节序转换为主机字节序。
- ntohs(network to host short):将unsigned short类型的网络字节序转换为主机字节序。
socket
Socket本身有"插座"的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。既然是文件就可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。在TCP/IP协议中,"IP地址+TCP或UDP端口号"唯一标识网络通讯中的一个进程。"IP地址+端口号"就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
socket常见API
cpp
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
特别注意inet_addr
inet_addr是一个库函数,用于将点分十进制的IP地址字如:192.168.1.1转换为一个32位的无符号整数(通常是网络字节序)。如果输入的字符串不是一个有效的IP地址,inet_addr会返回INADDR_NONE通常是(in_addr_t)-1。
cpp
server.sin_addr.s_addr=inet_addr(ser_ip.c_str());
sin_addr是sockaddr_in结构体中的一个in_addr结构体成员,用于存储IPv4地址。s_addr是in_addr结构体中的一个成员,它是一个无符号整数,用于存储IP地址(作为32位无符号整数)。
整行代码server.sin_addr.s_addr=inet_addr(ser_ip.c_str());
的意思是:将ser_ip
字符串(一个IPv4地址)转换为32位无符号整数,并将这个整数存储在server
的sin_addr.s_addr
成员中。
sockaddr
cpp
struct sockaddr {
//IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6.
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
//sa_data:这是一个14字节的字段,用于存储具体的地址信息。
//但是,由于这个字段是固定的长度,并且不直接支持IPv6等更复杂的地址类型,因此在实践中很少直接使用sockaddr结构体,而是使用更具体的结构体(如sockaddr_in或sockaddr_in6)。
sockaddr_in结构
IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。在编写网络程序时,通常会将sockaddr_in结构体转换为sockaddr指针,然后将其作为参数传递给系统调用(如bind、connect等)。这是因为这些系统调用使用sockaddr指针作为参数,以支持多种协议族。
UDP C/S
udp_server.cc
cpp
#include<iostream>
#include<string>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
//const uint16_t port =8080;
using namespace std;
string Usage(string prot)
{
cout<<"Usage:"<<prot<<"port"<<endl;
}
// ./udp_server prot
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[1]); return 1;
}
uint16_t prot=atoi(argv[1]);// uint16_t --16个二进制位
//1.创建socket,打开网络文件
//AF_INET--IPv4
//SOCK_DGRAM---Supports datagrams (connectionless, unreliable messages of a fixed maximum length)
int sock=socket(AF_INET,SOCK_DGRAM,0);
//2.给服务器绑定端口和IP
struct sockaddr_in local; //IPv4地址用sockaddr_in结构体表示
local.sin_family=AF_INET;
local.sin_port=htons(prot);//主机序列转化为网络序列
//local.sin_addr.s_addr 1.将人识别的点分10进制字符串风格的IP转化为4字节整数IP
//这个函数解决两个问题 2.大小端问题
local.sin_addr.s_addr=INADDR_ANY;//如果设置成某个IP,意味着只有发到这个IP的数据才会交到网络进程,这里需要所有发到该主机端口的数据
if(bind(sock,(struct sockaddr* )&local,sizeof(local))<0)
{
cerr<<"bind error"<<errno<<endl; return 2;
}
//3.提供服务 shell
bool quit=false;
char buffer[1024];
while(!quit)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//recvfrom的作用:receive messages from a socket
size_t cnt=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(cnt>0)
{
buffer[cnt]=0;
FILE* fp=popen(buffer,"r");//popen()函数通过创建管道、分叉和调用shell来打开进程
string echo_hello;
char line[1024]={0};
while(fgets(line,sizeof(line),fp)!=NULL)
{
echo_hello+=line;
}
pclose(fp);
cout<<"client# "<<buffer<<endl;
//transmit a message to another socket.
sendto(sock,echo_hello.c_str(),echo_hello.size(),0,(struct sockaddr*)&peer,len);
}
else{}
}
return 0;
}
udp_client.cc
cpp
#include<iostream>
#include<string>
#include<cerrno>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
using namespace std;
void Usage(string proc)
{
cout<<"Usage:\n\t"<<proc<<"server_ip server_port"<<endl;
}
// ./udp_client sercer_ip server_proc
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[3]);return 0;
}
//1.创建socket,打开网络文件
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
cerr<<"socket error:"<<errno<<endl; return 1;
}
//客户端不需要自己绑定,client发消息的时候OS会自动绑定,采用随机端口的方式
//和谁发消息
struct sockaddr_in server;
server.sin_family=AF_INET; //IPV4
server.sin_port=htons(atoi(argv[2]));//先转整数再转为网络序列
server.sin_addr.s_addr=inet_addr(argv[1]);//server的IP
while(1)
{
// string message;
// cout<<"输入#:";
// cin>>message;
cout<<"my shell $:";
char line[1024];
fgets(line,sizeof(line),stdin);
//给服务器发送消息
sendto(sock,line,strlen(line),0,(struct sockaddr*)&server,sizeof(server));
struct sockaddr_in tmp;
socklen_t len=sizeof(tmp);
char buffer[1024];
size_t cnt=recvfrom(sock,buffer,sizeof(buffer),0,(struct sockaddr*)&tmp,&len);
if(cnt>0)
{
//在网络通信中,只有报文的大小/字节流中字节的个数,没有字符串的概念
buffer[cnt]=0;
cout<<buffer<<endl;
}else{
}
}
return 0;
}
TCP C/S
tcp_server.cc
cpp
// ./tcp_server prot
int main(int argc,char* argv[])
{
if(argc!=2){
Usage(argv[1]); return 1;
}
//1.创建套接字
// socket本质是打开文件,仅仅有系统相关的内容
int listen_sock=socket(AF_INET,SOCK_STREAM,0);
if(listen_sock<0)
{
cerr<<"socktet error"<<errno<<endl; return 2;
}
//2.bind -- ip+port和文件信息关联--- 写入sockaddr中
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_addr.s_addr=INADDR_ANY;
local.sin_port=htons(atoi(argv[1]));
if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
{
cerr<<"bind error"<<errno<<endl; return 3;
}
//3.等待链接
//套接字设置listen状态本质是:不断的给用户提供一个建立连接的功能
const int back_log=3;//back_log -的挂起连接队列可能增长到的最大长度
if(listen(listen_sock,back_log)<0)
{
cerr<<"listen error"<<endl; return 4;
}
// signal(SIGCHLD,SIG_IGN);//在Linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源
//4.链接
for(;;)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);//获取连接到应用层
if(new_sock<0) continue;
//版本1.
//ServiceIO(new_sock); 单线程版本没人用
uint16_t cli_port=ntohs(peer.sin_port); //网络转主机序列
string cli_ip=inet_ntoa(peer.sin_addr);//主机地址(以网络字节顺序给出)转换为
//IPv4点分十进制格式的字符串
cout<<"get a new link: "<<cli_port<<"-"<<cli_ip<<endl;
//版本3.
Task t(new_sock);
ThreadPool<Task>::GetInstance()->PushTask(t);
//版本2.
// pid_t id=fork();
// if(id<0){
// continue;
// }
// //父进程打开的fd子进程会继承下去,父子进程中的哪个都要关闭不用的文件描述符
// else if(id==0)// child
// {
// close(listen_sock);
// //用这个不需要等待
// //退出的是子进程,子进程刚创建出来就退出了
// if(fork()>0)exit(0);
// //向后走的是孙子进程,爷孙进程不需要等待
// ServiceIO(new_sock);
// close(new_sock);//不关闭文件描述符,会造成文件描述符泄漏的问题
// exit(0);
// }
// else//father
// {
// waitpid(id,nullptr,0); //等待子进程
// close(new_sock);
// }
}
return 0;
}
tcp_client.cc
cpp
#include<iostream>
#include<sys/socket.h>
#include<cstring>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string>
using namespace std;
void Usage(string proc)
{
cout<<"Usage:"<<proc<<"server_ip server_prot"<<endl;
}
// ./tcp_client server_ip server_prot
int main(int argc, char* argv[])
{
if(argc!=3){
Usage(argv[2]); return 1;
}
string ser_ip=argv[1];
uint16_t ser_port=(uint16_t)atoi(argv[2]);
//1.创建socket
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
cerr<<"socket error"<<endl; return 2;
}
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
//(1). 将点分十进制的字符串风格的IP,转化成为4字节IP
//(2). 将4字节由主机序列转化成为网络序列
server.sin_addr.s_addr=inet_addr(ser_ip.c_str());
server.sin_port=htons(ser_port);
//2.connect
//系统层面构建一个请求报文发送出去
//网络层面 tcp链接的三次握手
if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0)
{
cerr<<"connect error"<<endl; return 3;
}
cout<<"connet success!!!"<<endl;
while(true)
{
cout<<"Please Enter#";
char buffer[1024];
fgets(buffer,sizeof(buffer)-1,stdin);
write(sock,buffer,strlen(buffer));
ssize_t s=read(sock,buffer,sizeof(buffer)-1);
if(s>0){
buffer[s]=0;
cout<<"server echo"<<buffer<<endl;
}
}
return 0;
}