在网络通信中,凡是我们所写的代码,采用的接口都是系统调用接口,编写的程序都叫用户层程序,我们接下来的工作就是在用户层自定义协议。在网络模型中就是应用层,那么就是要使用传输层的接口(但是有原始套接字可以绕过传输层)
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。
有了IP地址能够把消息发送到对方的机器上,但是跨网络传输还需要有一个其他的标识来区分出, 这个数据要给对方的哪个程序进行解析。
即IP在公网当中全网标识一台主机,发送的时候,不仅需要目的IP,通信也要自己的源IP也发过去,因为对方主机还要对你做出“回应”。
在你打开网页,访问百度的时候,实际上硬件只是一个载体,实际上通信的是你笔记本上的软件(浏览器进程),和对方服务器上的软件(服务器进程)。更准确的一点说,实际上是运行起来的进程进行通信,所以套接字的本质就是跨网络的进程间通信。
一个笔记本上,有很多进程,所有的进程并发的进行运行。所以通信的时候还需要一个东西来标识某个进程,标定进程的方式叫端口号。
IP标记某个全网唯一主机。
端口号标识主机内为一进程。那么IP+端口号就实现了标识全网内的唯一进程。而这个IP+端口号就是套接字。
服务器几乎永远不会关机,只会不断更新。虽然这里只简单的花了两个进程。但是这两台主机中充斥着大量的进程。公网IP保证了主机的唯一性,端口号保证了进程的唯一性。进程间通信,不同的进程看到了同一套资源,而跨网络进程通信,不同主机的进程就看到的是网络这个资源。
网络之中充斥着大量的套接字就要被管理起来
这里面有一个熟悉的file而file中存在一个
又指向这个socket指针
ops指针中存在着各种函数指针
pid表示唯一一个进程,并不是所有的进程都需要端口号,但是所有的进程在系统层面上都有一个pid。只有你这个进程是网络进程时才需要端口号。
一个进程可以绑定多个端口号
一个端口号只能用于一个进程
最开始收到数据的一定是计算机当中的网卡,然后自底向上交付。
低字节位在低地址处,叫做小端。
高字节位在低地址处,叫做大端。
假如你发数据,对方服务器可不知道你的数据你发的数据是大端还是小端,假如你的笔记本是小端,对方服务器是大端,那么服务器就会数据理解错误,这种情况肯定是存在,而且要被解决的。
网络规定,网络上跑的数据默认大端。假如你是小端机操作系统就会默认转为大端,收方默认接受的就是大端数据。
假设要发送0x1234abcd
大端就不做任何转换,小端机调用这些函数,将数据转换成大端序列。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址(点分十进制,四个.
,一个点隔开一个字节,每个范围为0-255)转换后准备发送
int socket(int domain, int type, int protocol);
domain代表域,即使用TCP或者UDP
套接字类别,流式套接字,数据报套接字,原始套接字
协议,操作系统使用默认行为
最重要的是返回值
可以这么理解,网卡也是一种文件,通信之前需要将文件打开,这里的socket函数等价于open,返回值等价一个文件描述符。
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
关联IP,端口号。服务器一般不发消息,永远是被动的,即绑定的,IP,端口号是客户端自己的。在系统方面表明,将IP信息与网络信息关联起来。
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同,操作系统实现了一种套接字接口,来解决不同套接字的编写,调用。
他就是sockaddr结构,就是用于将我们的IP,端口号等填入结构体,发给别人
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址
服务器被动的收发数据,所需要接口。
收数据
发数据
实际上这里的ip,可以不用输入,直接在填充的时候选择INADDR_ANY,这样在客户端输入任意IP,输入端口号,都可以访问服务器。
#pragma once #include<iostream> #include<stdlib.h> #include<string> #include<unistd.h> #include<sys/types.h> #include<sys/socket.h> #include<arpa/inet.h> #include<netinet/in.h> using namespace std; class udpServer { private: string ip; int port; int sock; public: udpServer(string _ip="127.0.0.1",int _port=8080) :ip(_ip) ,port(_port) {} void initServer() { //创建socket描述符,默认为3 sock=socket(AF_INET,SOCK_DGRAM,0); cout<<"sock:"<<sock<<endl; //填充信息到sockaddr _in中 struct sockaddr_in local; local.sin_family=AF_INET; //转成大端 local.sin_port=htons(port); //sockaddr中有一个sin_addr结构体,结构体中的saddr为ip //将ip转为char* local.sin_addr.s_addr=inet_addr(ip.c_str()); //绑定端口号 //可以让不同类型套接字,使用同一套接口,所以要强转 //为什么不用void*呢,套接字出现较早,void*还没定义。需要向前兼容,不能修改。 if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0) { cerr<<"bind error!\n"<<endl; exit(1); } } void strat() { char msg[64]={0}; for(;;) { //远端的信息 struct sockaddr_in end_point; socklen_t len=sizeof(end_point); //从网络接收数据,然后打印,拼凑一下返回给服务端 ssize_t s=recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len); if(s>0) { msg[s]='\0'; cout<<"client##"<<msg<<endl; string echo_string=msg; echo_string+="[注:服务器回显]"; sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len); } } } ~udpServer() { close(sock); } };
main函数只需要简单的启动即可
#include"udpServer.hpp" int main() { udpServer *up=new udpServer; up->initServer(); up->strat(); delete up; return 0; }
由于此时还没有客户端,那么怎么看到他运行起来了呢
用netstat -nlup命令,其中u代表udp,假如是 -bltp就是tcp。
服务端已启动。
#pragma once #include<iostream> #include<string> #include<unistd.h> #include<sys/types.h> #include<sys/socket.h> #include<arpa/inet.h> #include<netinet/in.h> using namespace std; class udpClient { private: string ip; int port; int sock; public: //连接服务器,服务器ip,port。 udpClient(string _ip="127.0.0.1",int _port=8080) :ip(_ip) ,port(_port) {} void initClient() { //创建socket描述符,默认为3 sock=socket(AF_INET,SOCK_DGRAM,0); cout<<"sock:"<<sock<<endl; //客户端不需要绑定 //填充信息到sockaddr _in中 //struct sockaddr_in local; //local.sin_family=AF_INET; //转成大端 //local.sin_port=htons(port); //sockaddr中有一个sin_addr结构体,结构体中的saddr为ip //将ip转为char* //local.sin_addr.s_addr=inet_addr(ip.c_str()); //绑定端口号 //可以让不同类型套接字,使用同一套接口,所以要强转 //为什么不用void*呢,套接字出现较早,void*还没定义。需要向前兼容,不能修改。 // if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0) // { // cerr<<"bind error!\n"<<endl; // _exit(1); //} } void strat() { string msg; struct sockaddr_in peer; peer.sin_family=AF_INET; peer.sin_port=htons(port); //点分十进制转成4字节,主机序列转成网络序列 peer.sin_addr.s_addr=inet_addr(ip.c_str()); for(;;) { cout<<"请输入"<<endl; cin>>msg; if(msg=="quit") { break; } //发去服务器 sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer)); char echo[128]; ssize_t s=recvfrom(sock,echo,sizeof(echo)-1,0,nullptr,nullptr); if(s>0) { echo[s]='\0'; cout<<"server###"<<echo<<endl; } } } ~udpClient() { close(sock); } };
main中初始化,然后启动
#include"udpClient.hpp" int main() { udpClient uc; uc.initClient(); uc.strat(); return 0; }
这里提到客户端不需要自己绑定,但是为什么服务器就需要绑定呢?
首先因为一个服务器的ip,port,是众所周知的,就比如http所对应80,https对应443,ssh对应22,mysql对应3306。一个服务器,面向数不清的客户端,一经更改,客户端就找不到服务器了。客户端不需要bind,客户端并不是只有一种,比如一个手机有微信,有qq,各种客户端进程,对应着不同的服务端,假如一个客户端进程绑定着1,2,3,4端口号,同时进来另一个客户端进程需要绑定1,2,3,4.由于一个端口号只能被一个进程绑定,那么就会导致冲突,客户端无法启动。服务器会有冲突问题吗,显然是会的,但是这个服务对应的端口号比如说,80我们就会想到http,那我们在编写程序就不会把80绑给我们的进程。
客户端的端口虽然对应唯一性,但是不需要明确必须是那个端口号。因为他需要IP和Port,让UDP客户端收数据,发送数据。系统会自动进行IP和端口号绑定。
客户端发数据,服务端收到,并回显到客户端。
而我们也可以,在main函数中传入参数,在命令行中带入ip与port,输入失败的时候,提示帮助手册。
客户端传入服务器的ip,port
127.0.0.1,通常用来进行网络通信代码的本地测试,一般把网络层全部自顶向下,自底向上,跑一遍。进行测试。
#ifndef __TCP__SERVER_H_ #define __TCP__SERVER_H_ #include<iostream> #include<string> #include<cstdlib> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<cstring> #include<unistd.h> using namespace std; class tcpServer { private: int port; int l_sock; public: tcpServer(int _port) :port(_port) ,l_sock(-1) {} void initServer() { l_sock=socket(AF_INET,SOCK_STREAM,0); if(l_sock<0) { cerr<<"socket error"<<endl; exit(2); } struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(port); local.sin_addr.s_addr=INADDR_ANY; if(bind(l_sock,(struct sockaddr*)&local,sizeof(local))<0) { cerr<<"bind error"<<endl; exit(3); } if(listen(l_sock,5)<0) { cerr<<"bind error"<<endl; exit(4); } } void service(int sock) { while(true) { //读取甚至可以用read //udp用recvfrom,tcp用recv //写可以用write //udp用sendto,tcp用send char buffer[24]={0}; size_t s=recv(sock,buffer,sizeof(buffer)-1,0); if(s>0) { buffer[s]={0}; cout<<"client#: "<<buffer<<endl; send(sock,buffer,strlen(buffer),0); } //不写这句他就会阻塞在send或recv,写上,当s==0时就退出 else if(s==0) { cout<<"client quit"<<endl; close(sock); break; } else{ cout<<"recv client data error"<<endl; break; } } close(sock); } void start() { sockaddr_in endpoint; while(true) { //重新获取一个socket,加上原来的此时共有两个 socklen_t len=sizeof(endpoint); int sock=accept(l_sock,(struct sockaddr*)&endpoint,&len); if(sock<0) { cerr<<"accept error"<<endl; continue; } cout<<"get a new link"<<endl; //当客户端退出,service也应该退出 service(sock); } } ~tcpServer() { close(sock); } }; #endif
全0表示任意IP都可以。
虽然没有客户端,但是远程登录工具可以登录服务器
也可以进行通信
#include<iostream> #include<string> #include<cstdlib> #include<cstring> #include<unistd.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> using namespace std; class tcpClient{ private: int svr_port; string svr_ip; int sock; public: tcpClient(string _ip="127.0.0.1",int port=8080) :svr_port(port) ,svr_ip(_ip) {} void initClient() { sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0) { cerr<<"sock error"<<endl; exit(2); } struct sockaddr_in svr; svr.sin_family=AF_INET; svr.sin_port=htons(svr_port); //点分十进制-》主机序列-》网络序列 svr.sin_addr.s_addr=inet_addr(svr_ip.c_str()); if(connect(sock,(struct sockaddr*)&svr,sizeof(svr))!=0) { cerr<<"connect error"<<endl; } } void start() { char msg[64]; while(true) { size_t s=read(0,msg,sizeof(msg)-1); if(s>0) { msg[s-1]=0; send(sock,msg,strlen(msg),0); size_t ss=recv(sock,msg,sizeof(msg)-1,0); if(ss>0) { msg[ss]=0; cout<<"server echo##"<<msg<<endl; } } } } ~tcpClient() { close(sock); } }; #endif
但是这个单进程版本,也就意味着当前进程不退出,别的进程就不能再次绑定。
Tcp套接字通信就像管道一样,管道也是流式服务
#ifndef __TCP__SERVER_H_ #define __TCP__SERVER_H_ #include<iostream> #include<string> #include<cstdlib> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> #include<cstring> #include<unistd.h> #include<signal.h> using namespace std; class tcpServer { private: int port; int l_sock; public: tcpServer(int _port) :port(_port) ,l_sock(-1) {} void initServer() { signal(SIGCHLD,SIG_IGN); l_sock=socket(AF_INET,SOCK_STREAM,0); if(l_sock<0) { cerr<<"socket error"<<endl; exit(2); } struct sockaddr_in local; local.sin_family=AF_INET; local.sin_port=htons(port); local.sin_addr.s_addr=INADDR_ANY; if(bind(l_sock,(struct sockaddr*)&local,sizeof(local))<0) { cerr<<"bind error"<<endl; exit(3); } if(listen(l_sock,5)<0) { cerr<<"bind error"<<endl; exit(4); } } void service(int sock) { while(true) { //读取甚至可以用read //udp用recvfrom,tcp用recv //写可以用write //udp用sendto,tcp用send char buffer[24]={0}; size_t s=recv(sock,buffer,sizeof(buffer)-1,0); if(s>0) { buffer[s]={0}; cout<<"client#: "<<buffer<<endl; send(sock,buffer,strlen(buffer),0); } //不写这句他就会阻塞在send或recv,写上,当s==0时就退出 else if(s==0) { cout<<"client quit"<<endl; close(sock); break; } else{ cout<<"recv client data error"<<endl; break; } } close(sock); } void start() { sockaddr_in endpoint; while(true) { //重新获取一个socket,加上原来的此时共有两个 socklen_t len=sizeof(endpoint); int sock=accept(l_sock,(struct sockaddr*)&endpoint,&len); if(sock<0) { cerr<<"accept error"<<endl; continue; } string cli_info=inet_ntoa(endpoint.sin_addr); cli_info+=":"; cli_info+=to_string(ntohs(endpoint.sin_port)); cout<<"get a new link"<<" "<<cli_info<<endl; //当客户端退出,service也应该退出 pid_t id =fork(); if(id==0) { //子进程关闭与否不影响 close(l_sock); service(sock); exit(0); } //1. waitpid(id,NULL,0),父进程阻塞了,所以可以设置为非阻塞 //2. 捕捉sigchild信号 //3. 忽略sigchild信号,自动由系统回收 //父进程必须关闭 //父进程不断获取链接,被子进程继承下去文件描述符很多,父进程可能就会不够用 close(sock); } } ~tcpServer() { close(l_sock); } }; #endif
#include"tcpServer.hpp" static void Usages(const string& str) { cout<<"Usage: port is "<<str<<endl; } int main(int argc,char* argv[]) { if(argc!=2) { Usages(argv[0]); exit(1); } tcpServer* tp=new tcpServer(atoi(argv[1])); tp->initServer(); tp->start(); delete tp; return 0; }
#ifndef __TCP__CLIENT_H_ #define __TCP__CLIENT_H_ #include<iostream> #include<string> #include<cstdlib> #include<cstring> #include<unistd.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<arpa/inet.h> using namespace std; class tcpClient{ private: int svr_port; string svr_ip; int sock; public: tcpClient(string _ip="127.0.0.1",int port=8080) :svr_port(port) ,svr_ip(_ip) {} void initClient() { sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0) { cerr<<"sock error"<<endl; exit(2); } struct sockaddr_in svr; svr.sin_family=AF_INET; svr.sin_port=htons(svr_port); //点分十进制-》主机序列-》网络序列 svr.sin_addr.s_addr=inet_addr(svr_ip.c_str()); if(connect(sock,(struct sockaddr*)&svr,sizeof(svr))!=0) { cerr<<"connect error"<<endl; } } void start() { char msg[64]; while(true) { cout<<"pleease enter"<<endl; size_t s=read(0,msg,sizeof(msg)-1); if(s>0) { msg[s-1]=0; send(sock,msg,strlen(msg),0); size_t ss=recv(sock,msg,sizeof(msg)-1,0); if(ss>0) { msg[ss]=0; cout<<"server echo##"<<msg<<endl; } } } } ~tcpClient() { close(sock); } }; #endif
#include"tcpClient.hpp" static void Usages(const string& str) { cout<<"Usage: port is "<<str<<endl; exit(0); } int main(int argc,char* argv[]) { if(argc!=3) { Usages(argv[0]); } tcpClient* tp=new tcpClient(argv[1],atoi(argv[2])); tp->initClient(); tp->start(); delete tp; return 0; }