在网络通信中,凡是我们所写的代码,采用的接口都是系统调用接口,编写的程序都叫用户层程序,我们接下来的工作就是在用户层自定义协议。在网络模型中就是应用层,那么就是要使用传输层的接口(但是有原始套接字可以绕过传输层)
在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)转换后准备发送
这个inet_ntoa函数返回一个char*,实际把它存储在静态存储区,也就是说,作为一个静态局部变量,虽然他的作用域依旧在函数内,但是生命周期却变成了整个文件。也就是说多线程,会有线程安全问题。新的一次会把老的一次覆盖掉。虽然在当前环境测试没有出现问题,可能是新版本添加了互斥锁。但是不推荐使用这个函数,可以用inet_ntop来代替。
int socket(int domain, int type, int protocol);
协议,操作系统使用默认行为
最重要的是返回值
可以这么理解,网卡也是一种文件,通信之前需要将文件打开,这里的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
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
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"<
// _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
服务器面对的客户很多,服务器一旦改了,客户端立马找不到,就无法访问服务器了。
客户端:
客户有很多客户端,如果绑定,就需要规定什么软件用什么端口,端口是标识进程的,一个端口只能对应一个进程,如果多个进程使用同一个端口,就会导致绑定是失败。并且这种让不同的公司进行沟通进行约定,是很不现实的。如果进行了bind会发生端口冲突,导致客户端无法启动
客户端需要唯一性,但不需要明确告诉你是哪个端口,因为也没有人去连接你,但是必需要IP和port。客户端使用udp服务器进行数据的交互之时,系统会自动进行Ip和端口号的绑定。
而我们也可以,在main函数中传入参数,在命令行中带入ip与port,输入失败的时候,提示帮助手册。
127.0.0.1,通常用来进行网络通信代码的本地测试,一般把网络层全部自顶向下,自底向上,跑一遍。进行测试。
#ifndef __TCP__SERVER_H_
#define __TCP__SERVER_H_
#include
#include
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
#ifndef __TCP__SERVER_H_
#define __TCP__SERVER_H_
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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);
}
}
static 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);
}
static void* serviceRoutine(void* args)
{
//让主线程不需要阻塞等待
pthread_detach(pthread_self());
cout<<"create new thread"<<endl;
int *p=(int*)args;
int sock=*(int*)args;
service(sock);
delete p;
}
//没有使用任何成员方法,成员属性
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;
pthread_t tid;
int *p=new int(sock);
pthread_create(&tid,nullptr,serviceRoutine,(void*)p);
//不能关闭文件描述符,因为他们共用一个
//当客户端退出,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
#include
#include
#include
#include
#include
#include
#include
#include
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;
}
而实际上,系统是可能存在大量客户端,此时创建进程线程,已不再是耗费资源的主要原因,而是在各个执行流切换所带来的效率问题。