文章目录
字节序
字节序:cpu对内存中数据进行存取的顺序
主机字节序的分类:小端、大端
小端:低地址存低位
大端:低地址存高位
编写代码判断主机字节序:
#include<iostream>
using namespace std;
void check_sys1()
{
int a = 1;
char* b = (char*)&a;
if (*b == 1)
cout << "小端" << endl;
if (*b == 0)
cout << "大端" << endl;
}
void check_sys2()
{
//联合类型的所有数据共用一块内存,内存大小根据最大的数据类型决定
union UN {
int a;
char b;
}u;
u.a = 1;
if(u.b==1)
cout << "小端" << endl;
if(u.b==0)
cout << "大端" << endl;
}
int main()
{
check_sys1();
check_sys2();
return 0;
}
主机字节序对网络通信的影响:如果通信两端主机字节序不同,可能会造成数据二义性。
解决方案:订立网络通信字节序标准,规定网络中的数据都按照网络字节序进行存取。网络字节序-----其实是大端字节序。
发送方将数据转换成网络字节序后进行发送,接收方根据自己的的主机字节序将接收到的数据进行转换。
字节序只针对存储单元大于一个字节的数据类型。
注意:字符串实际上是单字节存储,所以不需要转换。
套接字编程
socket 套接字编程:网络通信程序的编写
分类:UDP协议通信程序的/TCP协议通信程序
区别:
UDP协议:用户数据报协议
特点:无连接,不可靠,面向数据报
应用场景:实时性要求大于安全性要求----例如视频传输
TCP协议:传输控制协议
特点:面向连接,可靠,面向字节流
应用场景:安全性要求大于实时性要求----例如文件传输
客户端与服务端
在网络通信程序中,通信两端被分为客户端和服务端,
客户端:提供给客户的通信段,通常是通信程序中主动发起请求的一端。
客户端必须提前知道服务端的地址信息(ip地址+port端口)才能发送请求,通常是被提前写在应用程序中,并且通常是固定不变的。
服务端:通常是指被动接受请求,提供服务的通信端。
★ netstat命令 ★
netstat命令:查看当前网络状态信息
-a:查看所有
-t :查看TCP信息
-u:查看UDP信息
-n:不以服务名称显示,以具体地址端口显示
-p:查看当前网络状态对应的进程
UDP通信程序
通信流程:
接口:
1、创建套接字:int socket(int domain,int type,int protocol);
domain:地址域类型----指定使用的是什么样的地址结构:AF_INET----IPV4通信,使用IPV4地址结构
type:套接字类型;SOCK_STREAM:流式套接字 / SOCK_DGRAM:数据报套接字注意:TCP协议必须使用SOCK_STREAM,UDP必须使用SOCK_DGRAM
protocol:本次通信所使用的协议;IPPROTO_IP=6 / IPPROTO_UDP=17(可以使用宏,也可以使用数字)
返回值:成功,返回一个文件描述符----操作句柄;失败,返回-1。
2、为套接字绑定地址信息:int bind(int sockfd,struct sockaddr* addr,socklen_t addrlen);
sockfd:socket() 创建套接字返回的操作句柄
addr:当前绑定的地址信息
socklen_t addrlen:地址信息长度
返回值:成功返回0;失败返回-1。
3、接收数据:ssize_t recvfrom(int sockfd,void* buf,int len,int flag,struct addr* srcaddr,socklen_t * addrlen);
ssize_t:有符号int;
size_t:无符号int
sockfd:创建套接字返回的操作句柄
buf:用于存放接收到的数据的空间地址
len:需要接受的数据长度
flag:选项标志,通常默认为0,表示阻塞接收
srcaddr:本条数据的源端地址信息
addrlen:输入输出参数,指定要接收多长的地址长度,但实际可能并没有那么长,所以还会返回实际接收到的地址长度
返回值:成功,返回实际接收到的数据长度;失败或出错返回-1。
4、发送数据:ssize_t sendto(int sockfd,void *data,int len,int flag,struct sockaddr* peeraddr,socklen_t addrlen);
sockfd:操作句柄
data:要发送的数据的首地址
len:要发送的数据长度
flag:默认为0,阻塞发送
peeraddr:对端地址信息
addrlen:地址结构长度
返回值:成功,返回实际发送的数据长度;失败,返回-1。
5、关闭套接字:int close(int fd);
fd:操作句柄
流程外的重要接口:
字节序转换接口:
unint32_t htonl(uint32_t hostlong);----32位数据主机字节序到网络字节序的转换
unint16_t htons(uint32_t hostshort);----16位数据主机字节序到网络字节序的转换
unint32_t ntohl(uint32_t netlong);----32位数据网络字节序到主机字节序的转换
unint16_t ntohs(uint32_t netshort);----16位数据网络字节序到主机字节序的转换
注意:port端口转换使用htons/ntohs,ip转换使用htonl/ntohl,不能混用。将字符串点分十进制IP地址转换为整型网络字节序IP地址:
“192.168.2.2”————》0xc0a80202
in_addr_t inet_addr(const char* cp);将网络字节序IP地址转换为字符串点分十进制IP地址:
0xc0a80202————》“192.168.2.2”
char* inet_ntoa(struct in_addr in);以上接口仅限于IPV4地址使用
不限于IPV4的地址转换:
int inet_pton(int af,const char* src,void* dst);
const char* inet_ntop(int af,void* src,char* dst,socklen_t size);
(1)这两个函数的af参数既可以是AF_INET(ipv4)也可以是AF_INET6(ipv6)。如果,以不被支持的地址族作为af参数,这两个函数都返回一个错误,并将errno置为EAFNOSUPPORT.
(2)第一个函数尝试转换由src指针所指向的字符串,并通过dst指针存放二进制结果,若成功则返回值为1,否则如果所指定的af而言输入字符串不是有效的表达式格式,那么返回值为0.
(3)inet_ntop进行相反的转换,从数值格式(src)转换到表达式(dst)。inet_ntop函数的src参数不可以是一个空指针。调用者必须为目标存储单元分配内存并指定其大小,调用成功时,这个指针就是该函数的返回值。size参数是目标存储单元的大小,以免该函数溢出其调用者的缓冲区。如果size太小,不足以容纳表达式结果,那么返回一个空指针,并置为errno为ENOSPC。
服务端代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>//字节序转换接口头文件
#include<netinet/in.h>//地址结构/协议类型头文件
#include<sys/socket.h>//套接字接口文件
int main()
{
//1.创建套接字
//int socket(地址域类型,套接字类型,协议类型);
int sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(sockfd<0)
{
perror("socket error:");
return -1;
}
//2.为套接字绑定地址信息
//int bind(操作句柄,地址结构信息,地址长度);
struct sockaddr_in addr;//定义ipv4地址结构
addr.sin_family=AF_INET;
addr.sin_port=htons(9000);//设置端口
addr.sin_addr.s_addr=inet_addr("192.168.85.128");
int len=sizeof(addr);
int ret=bind(sockfd,(struct sockaddr*)&addr,len);
if(ret<0)
{
perror("bind error:");
return -1;
}
while(1)
{
//3.接收数据
//recvfrom(句柄,空间,长度,标志,对端地址,地址长度)
char buf[1024]={0};
struct sockaddr_in paddr;
int len=sizeof(struct sockaddr_in);
ret= recvfrom(sockfd,buf,1023,0,(struct sockaddr*)&paddr,&len);
if(ret<0)
{
perror("recv error:");
return -1;
}
uint16_t cport=ntohs(paddr.sin_port);
char* cip=inet_ntoa(paddr.sin_addr);
printf("client-[%s:%d]client say:%s\n",cip,cport,buf);
//4.回复数据
memset(buf,0x00,1024);
printf("server say:");
fflush(stdout);
fgets(buf,1023,stdin);
ret=sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&paddr,len);
if(ret<0)
{
perror("send error:");
return -1;
}
}
//5.关闭套接字
close(sockfd);
return 0;
}
客户端代码
头文件:
/*
* 封装一个udp_socket类
* 通过实例化的对象调用对应的成员接口实现udp客户端及服务端搭建
*/
#include<iostream>
#include<string>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<cstdio>
using namespace std;
class UdpSocket{
private:
int _sockfd;
public:
UdpSocket():_sockfd(-1)
{}
bool Socket()
{
_sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(_sockfd<0){
perror("socket error:");
return false;
}
return true;
}
bool Bind(std::string &ip,uint16_t port){
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr(ip.c_str());
socklen_t len=sizeof(struct sockaddr_in);
int ret;
ret=bind(_sockfd,(struct sockaddr*)&addr,len);
if(ret<0){
perror("bind error:");
return false;
}
return true;
}
bool Send(std::string &data,const std::string &ip,int port)
{
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr(ip.c_str());
socklen_t len=sizeof(struct sockaddr_in);
int ret=sendto(_sockfd,data.c_str(),data.size(),0,(struct sockaddr*)&addr,len);
if(ret<0){
perror("sendto error:");
return false;
}
return true;
}
bool Recv(std::string *buf,std::string *ip=NULL,int *port=NULL){
struct sockaddr_in addr;
socklen_t len=sizeof(struct sockaddr_in);
char tmp[4096]={0};
int ret=recvfrom(_sockfd,tmp,4096,0,(struct sockaddr*)&addr,&len);
if(ret<0){
perror("recvfrom error:");
return false;
}
buf->assign(tmp,ret); //申请ret长度的空间,并且将tmp的数据拷贝过去
if(ip!=NULL){
*ip=inet_ntoa(addr.sin_addr);
}
if(port!=NULL){
*port=ntohs(addr.sin_port);
}
return true;
}
bool Close()
{
if(_sockfd!=-1){
close(_sockfd);
}
return true;
}
};
主程序:
#include"udp_socket.hpp"
#define CHECK_RET(q) if((q)==false){return -1;}
int main(){
UdpSocket sock;
//1.创建套接字
CHECK_RET(sock.Socket());
//2.绑定地址信息(客户端不推荐执行这一操作)
while(1){
//3.发送数据
std::cout<<"client say:";
std::string buf;
std::cin>>buf;
CHECK_RET(sock.Send(buf,"192.168.85.128",9000));
//4.接收数据
buf.clear();
CHECK_RET(sock.Recv(&buf));
std::cout<<"server say:"<<buf<<endl;
}
//5.关闭套接字
sock.Close();
return 0;
}
输出结果:
TCP通信程序
通信流程:
创建连接的 原理:
- 服务端创建一个套接字 S1(包含sip+sport+tcp) ,并将其置于listen状态,开始处理客户端的请求(S1仅用于接收新的客户端连接);
- 客户端向服务端发送一个连接请求,服务端通过复制 S1 为该客户端创建一个新的套接字(包含sip+sport+dip+dport+tcp);
接口:
1、创建套接字:int socket(int domain,int type,int protocol);
2、绑定地址信息:int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
3、开始监听:int listen(int sockfd,int backlog);
sockfd:套接字描述符
backlog:服务端在同一时间能够处理的最大连接数
内核中有一个已完成连接队列----存储已经完成的客户端连接,如果这个队列已满,操作系统将不能再接收新的连接请求。
已完成连接队列的大小=backlog+1;
syn泛洪攻击:向服务端大量发送连接请求,阻碍服务端正常运转;解决办法:已完成连接队列,防火墙
4、客户端发送连接请求:int connect(int sockfd,struct sockaddr*srvaddr,socklen_t len);
sockfd: 套接字描述符
srvaddr: 服务端地址信息
len: 地址长度
返回值:成功返回0;失败返回-1。
5、服务端获取新建连接;int accept(int s, struct sockaddr *cliaddr, socklen_t *addrlen);
sockfd:监听套接字----服务端最早创建的,只用于获取新链接的套接字
cliaddr: 新的连接的客户端地址信息
addrlen:输入输出参数,指定地址信息长度,以及返回实际长度
返回值:新建连接的描述符----往后与客户端的通信都通过这个描述符完成。
6、收发数据:tcp通信因为socket中含有完整的五元组,所以收发数据数据都不需要指定地址
ssize_t send(int sockfd, void *data, int len, int flag);
sockfd: 描述符
data:要发送的数据
len:数据长度
flag:0----默认阻塞
返回值:成功返回实际发送的长度;失败返回-1;连接断开会触发异常
ssize_t recv(inst sockfd,void *buf,int len,int flag);
sockfd:描述符
buf:存放接收到的数据的空间地址
flag:0-----阻塞接收
返回值:成功返回实际收到的数据长度;出错返回-1;连接断开返回0.
7、关闭套接字:int close(int sockfd);
代码实例:
封装的socket类:
```cpp
#include<cstdio>
#include<unistd.h>
#include<iostream>
#include<string>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#define CHECK_RET(q) if((q)==false){return -1;}
#define LISTEN_BACKLOG 5
class TcpSocket{
private:
int _sockfd;
public:
TcpSocket():_sockfd(-1){}
bool Socket(){
_sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(_sockfd<0){
perror("socket error:");
return false;
}
return true;
}
bool Bind(const std::string &ip,const uint16_t port){
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr(&ip[0]);
socklen_t len=sizeof(sockaddr);
int ret=bind(_sockfd,(sockaddr*)&addr,len);
if(ret<0){
perror("bind error:");
return false;
}
return true;
}
bool Listen(int backlog = LISTEN_BACKLOG){
//listen(描述符,同一时间最大连接数);
int ret=listen(_sockfd,backlog);
if(ret<0){
perror("listen error");
return false;
}
return true;
}
bool Connect(const std::string &ip,const uint16_t port){
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr(&ip[0]);
socklen_t len=sizeof(sockaddr);
int ret=connect(_sockfd,(sockaddr*)&addr,len);
if(ret<0){
perror("connect error");
return false;
}
return true;
}
bool Accept(TcpSocket* sock,std::string *ip=NULL,uint16_t *port=NULL){
//int accept(监听套接字,获取客户端地址,长度)
sockaddr_in addr;
socklen_t len=sizeof(sockaddr_in);
int newfd=accept(_sockfd,(sockaddr*)&addr,&len);
if(newfd<0){
perror("accept error");
return false;
}
sock->_sockfd=newfd;
if(ip!=NULL){
*ip=inet_ntoa(addr.sin_addr);
}
if(port!=NULL){
*port=ntohs(addr.sin_port);
}
return true;
}
bool Recv(std::string *buf){
//int recv(描述符,空间,数据长度,标志位)
//返回值:实际h获取的大小 0---连接断开;-1------出错了
char tmp[4096]={0};
int ret=recv(_sockfd,tmp,4096,0);
if(ret<0){
perror("recv error");
return false;
}else if(ret==0){
printf("连接断开!\n");
return false;
}
buf->assign(tmp,ret);
return true;
}
bool Send(const std::string &data){
//int send(描述符,数据,长度,标志位);
int total=0;
while(total<data.size()){
int ret=send(_sockfd,&data[0]+total,data.size()-total,0);
if(ret<0){
perror("send error");
return false;
}
total+=ret;
}
return true;
}
bool Close(){
if(_sockfd!=-1){
close(_sockfd);
}
return true;
}
};
服务端代码:
#include "tcpsocket.hpp"
int main(int argc,char* argv[]){
//命令行: ./tcp_srv 192.168.2.2 9000
if(argc!=3){
printf("格式:./tcp_src 192.168.2.2 9000\n");
return -1;
}
std::string srvip=argv[1];
uint16_t srvport=std::stoi(argv[2]);
TcpSocket lst_sock;//监听套接字,仅用于接受新连接
//1.创建套接字
CHECK_RET(lst_sock.Socket());
//2.绑定地址信息
CHECK_RET(lst_sock.Bind(srvip,srvport));
//3.开始监听
CHECK_RET(lst_sock.Listen());
while(1){
//4.获取新连接
TcpSocket cli_sock;//客户端套接字
std::string cli_ip;//客户端ip地址
uint16_t cli_port; //客户端端口
bool ret=lst_sock.Accept(&cli_sock,&cli_ip,&cli_port);
if(ret==false){
//客户端发生错误,服务端不能退出,继续处理下一个客户端的请求
continue;
}
std::cout<<"获取新建连接:"<<cli_ip<<":"<<cli_port<<std::endl;
//5.收发数据---使用获取的新建套接字进行通信
std::string buf;
ret=cli_sock.Recv(&buf);
if(ret==false){
cli_sock.Close();
continue;
}
std::cout<<"client say:"<<buf<<std::endl;
buf.clear();
std::cout<<"server say:";
std::cin>>buf;
ret=cli_sock.Send(buf);
if(ret==false){
cli_sock.Close();
}
}
//6.关闭套接字
lst_sock.Close();
return 0;
}
客户端代码:
#include "tcpsocket.hpp"
int main(int argc, char *argv[])
{
//通过参数传入要连接的服务端的地址信息
if (argc != 3) {
printf("输入格式: ./tcp_cli srvip srvport\n");
return -1;
}
std::string srvip = argv[1];
uint16_t srvport = std::stoi(argv[2]);
TcpSocket cli_sock;
//1. 创建套接字
CHECK_RET(cli_sock.Socket());
//2. 绑定地址信息(不推荐)
//3. 向服务端发起连接
CHECK_RET(cli_sock.Connect(srvip, srvport));
while(1){
//4. 收发数据
std::string buf;
std::cout<<"client say:";
std::cin>>buf;
CHECK_RET(cli_sock.Send(buf)); //发送数据
buf.clear();
CHECK_RET(cli_sock.Recv(&buf)); //接收数据
std::cout<<"server say:"<<buf<<std::endl;
}
//5. 关闭套接字
CHECK_RET(cli_sock.Close());
return 0;
}
出现问题:
上述代码虽能正常运行,但同时也存在很大的问题:
accept、recv和send都是阻塞接口,任意一个接口的调用都有可能会导致服务端流程阻塞
本质原因:当前的服务端不知道什么时候又新连接到来,什么时候哪个客户端有数据到来,因此流程只能固定的去调用接口,但是这种调用方式可能会造成阻塞
解决方案:
多执行流并发处理----为每个客户端创建一个执行流负责这个客户端的通信
好处:
- 即使主线程被卡在获取新连接这一步,也不会影响其他执行流中客户端的通信
- 某个客户端阻塞,不会影响主线程和其他线程
具体操作:在主线程中获取新建连接,一旦获取到了就创建一个执行流,通过这个新建连接与客户端进行通信
多线程:普通线程与主线程数据共享,指定入口函数执行;主线程不能随意释放套接字,因为资源共享,一旦释放,其他线程无法使用
多进程:子进程复制了父进程,但是数据独有;要注意僵尸进程的处理,注意父子进程数据独有,父进程用不到套接字资源因此创建子进程之后要记得释放掉,否则会造成资源泄露。
优化后的代码
多线程:服务端
#include "tcpsocket.hpp"
#include <pthread.h>
void *thr_entry(void *arg)
{
bool ret;
TcpSocket *clisock = (TcpSocket*)arg;
while(1) {
//5. 收发数据--使用获取的新建套接字进行通信
std::string buf;
ret = clisock->Recv(&buf);
if (ret == false) {
clisock->Close();
delete clisock;
return NULL;
}
std::cout << "client say: " << buf << std::endl;
buf.clear();
std::cout << "server say: ";
std::cin >> buf;
ret = clisock->Send(buf);
if (ret == false) {
clisock->Close();
delete clisock;
return NULL;
}
}
clisock->Close();
delete clisock;
return NULL;
}
int main(int argc, char *argv[])
{
//通过程序运行参数指定服务端要绑定的地址
// ./tcp_srv 192.168.2.2 9000
if (argc != 3) {
printf("usage: ./tcp_src 192.168.2.2 9000\n");
return -1;
}
std::string srvip = argv[1];
uint16_t srvport = std::stoi(argv[2]);
TcpSocket lst_sock;//监听套接字
//1. 创建套接字
CHECK_RET(lst_sock.Socket());
//2. 绑定地址信息
CHECK_RET(lst_sock.Bind(srvip, srvport));
//3. 开始监听
CHECK_RET(lst_sock.Listen());
while(1) {
//4. 获取新建连接
TcpSocket *clisock = new TcpSocket();
std::string cliip;
uint16_t cliport;
bool ret = lst_sock.Accept(clisock, &cliip,&cliport);
if (ret == false) {
continue;
}
std::cout<<"get newconn:"<< cliip<<"-"<<cliport<<"\n";
//创建线程专门负责与指定客户端的通信
pthread_t tid;
pthread_create(&tid, NULL, thr_entry, (void*)clisock);
pthread_detach(tid);
}
//6. 关闭套接字
lst_sock.Close();
return 0;
}
多进程:服务端
#include "tcpsocket.hpp"
#include <signal.h>
#include <sys/wait.h>
void sigcb(int no)
{
while(waitpid(-1, NULL, WNOHANG) > 0);
}
void worker(TcpSocket &clisock)
{ //child process
bool ret;
while(1) {
//5. 收发数据--使用获取的新建套接字进行通信
std::string buf;
ret = clisock.Recv(&buf);
if (ret == false) {
clisock.Close();
exit(0);
}
std::cout <<"client say: "<<buf<<std::endl;
buf.clear();
std::cout << "server say: ";
std::cin >> buf;
ret = clisock.Send(buf);
if (ret == false) {
clisock.Close();
exit(0);
}
}
clisock.Close();//释放的是子进程的clisock
exit(0);
return;
}
int main(int argc, char *argv[])
{
//通过程序运行参数指定服务端要绑定的地址
// ./tcp_srv 192.168.2.2 9000
if (argc != 3) {
printf("usage: ./tcp_src 192.168.2.2 9000\n");
return -1;
}
signal(SIGCHLD, SIG_IGN);
//signal(SIGCHLD, sigcb);
std::string srvip = argv[1];
uint16_t srvport = std::stoi(argv[2]);
TcpSocket lst_sock;//监听套接字
//1. 创建套接字
CHECK_RET(lst_sock.Socket());
//2. 绑定地址信息
CHECK_RET(lst_sock.Bind(srvip, srvport));
//3. 开始监听
CHECK_RET(lst_sock.Listen());
while(1) {
//4. 获取新建连接
TcpSocket clisock;
std::string cliip;
uint16_t cliport;
bool ret = lst_sock.Accept(&clisock, &cliip,&cliport);
if (ret == false) {
continue;
}
std::cout<<"get newconn:"<< cliip<<"-"<<cliport<<"\n";
pid_t pid = fork();
if (pid < 0) {
clisock.Close();
continue;
}else if (pid == 0) {
worker(clisock);
}
//父子进程数据独有,父进程关闭不会对子进程造成影响
clisock.Close();//释放的是父进程中的clisock
}
//6. 关闭套接字
lst_sock.Close();
return 0;
}