一 、预备知识
1、 理解源IP地址和目的IP地址
在IP数据包中有两个IP地址,一个是源IP地址,一个是目的IP地址。IP地址负责把数据从一台主机硬件传输到另一台主机硬件,IP地址标识这台主机全网的唯一性,硬件的唯一性
2 端口号(port)
IP地址是标识全网唯一一台主机,而一台主机有很多进程,所以端口号用来标识一台主句中特定的进程。所以IP地址+端口号=全网唯一一个进程(ip+端口号叫做套接字)
3 端口号和进程ID之间的关系
我们在系统编程中了解到每一个进程都有一个唯一的Id叫做pid,此处端口号也是唯一描述一个进程。
进程的PID是必须有的,只有网络进程才有端口号。
一个端口号只能被一个进程占用.端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;一个进程绑定多个端口号,但是一个端口号不能被多个进程绑定。
4 源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要
发给谁”;
5 TCP与UDP协议
tcp
特点:
传输层协议
有连接
可靠传输
面向字节流
udp
特点:
传输层协议
无连接
不可靠传输
面向数据报
udp和tcp相比较udp的传输效率比较快。
6 网络字节序
之前我们了解到数据存储由大小端之分,不同主机之间也有可能由大小端之分,所以在网络传输过程中,如果源主机是大端再传输数据,而目的主机是小端机器且不知道源主机接收到数据会解释出错,那么怎么解决呢?网络规定所有再网络上跑的数据都是大端的。不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
二、socket常见接口
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain
Socket. 然而, 各种网络协议的地址格式并不相同.操作系统为了用一套接口套接字接口来完成所有套接字种类或不同种类之间的通信问题,为了满足这个要求linux网络设计了sockaddr这套接口
这3中结构相同点都有一个16位的地址类型,套接字常用结构为第二种struct sockaddr_in其中16位端口号和32位IP地址称为套接字,常见socket接口中参数都为这个struct sockaddr* address,但是我们经常使用的是struct sockaddr_in这个结构体只需在传参过程中强转为struct sockaddr这个就行,这是用一套接口完成了不同程序的转换,当传入struct sockaddr_in使用的是IPV4的通信方式,当传入第三种结构体使用的是IPV6的通信方式。
socket常见API
头文件:
#include<sys/types.h>
#include<sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
参数:
domian:采用的是哪一个协议,AF_INET(IPV4)经常使用的,AF_INET6(IPV6)
type采用地套接字类别,sock_streaM(大写 流式套接字),sock_dgran(大写 用户数据报套接字)
protocol默认为0.
其返回值是一个文件描述符,因为再linux中一切接文件,要实现网络通信首先要打开网卡设备创建所需要地数据结构,这些数据结构再linux中看来也是文件。
创建一个套接字就是在打开一个文件后,打开一个网络文件这是一个进程,这就成了一个网络进程,网络进程在通信时必须有一个端口号与其绑定,将系统信息和IP地址绑定。
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
参数:
socket打开网络文件的文件描述符
struct socketaddr:参数通常是struct socketaddr_in类型里边包括IP地址和端口号
address_len:传入address对应的长度。
函数执行成功返回0,否则返回-1, 并设置错误代码.
socketaddr_in结构体
sin_family是协议家族采用的是IPV4所以一般值为AF_INET,其中In_port就是端口号16位,in_addr就是一个结构体
In_addr结构体
s_addr是一个32位的IP地址
因为udp套接字通信不需要建立连接,所以就可以直接收发文件。
用于接受
ssize_t recvform(int sockfd,voidbuff,size_t len,int flags,struct sockaddrsrc_addr,socklen_t *addrlen);
参数;
sockfd是一个文件描述符,buf读取数据的缓冲区,len期望读多少数据代表的是缓冲区的大小,flags读数据属于io读数据不一定有数据可读(当读取条件不成立,进程就要等待阻塞,默认为0为阻塞状态),后边的src_addr和addrlen是一个输出型参数包含有源主机的IP地址和源端口port
用于发送
ssize_t sendto(int sockfd,const void buff,size_t len,int flags,struct sockaddrsrc_addr,socklen_t *addrlen);
参数:
sockfd是一个文件描述符,buff表示发生的内容,len发生数据的长度,flags写数据属于io写数据不一定可以写(当写条件不成立,进程就要等待阻塞,默认为0为阻塞状态)后边的src_addr和addrlen是一个输入参数包含有源主机的IP地址和源端口port
地址转换函数
在基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
in_addr转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr。
UDP通用套接字
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<iostream>
#include<string>
using namespace std;
class SocketSever{
private:
//string ip;
int post;
int sock;//文件描述符,创建套接字返回值
public:
SocketSever(int _post=8080):
post(_post)
{
}
void Init(){
sock=socket(AF_INET,SOCK_DGRAM,0);
cout<<"sock:"<<sock<<endl;
struct sockaddr_in in_addr;//IP地址和端口号在内核层面绑定
in_addr.sin_family=AF_INET;//设置协议家族一般为AF_INET IPV4
in_addr.sin_port=htons(post);//设置端口号,因为是网络通信所以要把主机序列转成网络序列
//in_addr.sin_addr.s_addr=inet_addr(ip.c_str());//设置IP地址,将字符串ip转成点分十进制的网络IP地址,因为string是一个类而inet_addr参数是一个字符串所以使用了c_str()转换。
in_addr.sin_addr.s_addr=INADDR_ANY;
if(bind(sock,(const struct sockaddr*)&in_addr,sizeof(in_addr))<0){
cout<<"绑定失败。。。"<<endl;
exit(1);
}
}
void star(){
char buff[64];
while(1){
struct sockaddr_in addr;
socklen_t len=sizeof(addr);//实际读到的大小
ssize_t size=recvfrom(sock,buff,sizeof(buff)-1,0,(struct sockaddr*)&addr,&len);//接受
if(size>0){
buff[size]=0;
cout<<"client:"<<buff<<endl;
string str="sever 已经收到数据。。。";
size_t s=sendto(sock,str.c_str(),sizeof(str)-1,0,(struct sockaddr*)&addr,len);
}
}
}
~SocketSever(){
close(sock);
}
};
int main(int argc,char *argv[]){
SocketSever ss(atoi(argv[2]));
ss.Init();
ss.star();
return 0;
}
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<iostream>
#include<string>
using namespace std;
class Socketclient{
private:
string ip;
int post;
int sock;//文件描述符,创建套接字返回值
public:
//ip是对应client的IP地址和端口号
Socketclient(string ip_="127.0.0.1",int _post=8080):
ip(ip_),
post(_post)
{
}
//客户端不需要绑定只需创建套接字
void Init(){
sock=socket(AF_INET,SOCK_DGRAM,0);
cout<<"sock:"<<sock<<endl;
//struct sockaddr_in in_addr;//IP地址和端口号在内核层面绑定
//in_addr.sin_family=AF_INET;//设置协议家族一般为AF_INET IPV4
//in_addr.sin_port=htons(post);//设置端口号,因为是网络通信所以要把主机序列转成网络序列
//in_addr.sin_addr.s_addr=inet_addr(ip.c_str());//设置IP地址,将字符串ip转成点分十进制的网络IP地址,因为string是一个类而inet_addr参数是一个字符串所以使用了c_str()转换。
//if(bind(sock,(const struct sockaddr*)&in_addr,sizeof(in_addr))<0){
//cout<<"绑定失败。。。"<<endl;
//exit(1);
//}
}
//client是先收后发,client是先发后收
void star(){
//char buff[64];
string str;
while(1){
cout<<"请输入要发送的内容:";
cin>>str;
struct sockaddr_in in_addr;//IP地址和端口号在内核层面绑定
in_addr.sin_family=AF_INET;//设置协议家族一般为AF_INET IPV4
in_addr.sin_port=htons(post);//设置端口号,因为是网络通信所
in_addr.sin_addr.s_addr=inet_addr(ip.c_str());//设置IP地址,将字符串ip转成点分十进制的网络IP地址,因为string是一个类而inet_addr参数是一个字符串所以使用了c_str()转换。以要把主机序列转成网络序列
//
size_t s=sendto(sock,str.c_str(),sizeof(str)-1,0,(struct sockaddr*)&in_addr,sizeof(in_addr));
if(s<0){
cout<<"传输错误"<<endl;
}
char buff[64];
struct sockaddr_in addr;
socklen_t t=sizeof(addr);
ssize_t size=recvfrom(sock,buff,sizeof(buff)-1,0,(struct sockaddr*)&addr,&t);
if(size>0){
buff[size]=0;
cout<<"服务器反馈。。。"<<endl;
}
}
//struct sockaddr_in addr;
//socklen_t t struct sockaddr*)&in_addr,sizeof(in_addr)en=sizeof(addr);//实际读到的大小
//ssize_t size=recvfrom(sock,buff,sizeof(buff)-1,0,(struct sockaddr*)&addr,&len);//接受
//if(size>0){
//buff[size]=0;
//cout<<"client:"<<buff<<endl;
//string str="client 已经收到数据。。。";
//size_t s=sendto(sock,str.c_str(),sizeof(str)-1,0,(struct sockaddr*)&addr,len);
//}
//}
}
~Socketclient(){
close(sock);
}
};
void up(string str){
cout<<str<<"请输入IP地址和端口号"<<endl;
}
int main(int argc,char *argv[]){
if(argc!=3){
up(argv[0]);
exit(1);
}
Socketclient ss(argv[1],atoi(argv[2]));
ss.Init();
ss.star();
return 0;
}
说明:
一般sever的IP地址和post不能轻易更改,是确定的众所周知的,因为一旦更改客户端就会连接不上
客户端不需要强bind,但是需要IP和post,为什么不需要bind?
1 你在bind的时候,很容易造成客户端启动失败
2 客户端需要唯一的端口号,但是不需要明确需要是哪个端口号
但是需要IP和port:client的udp,recv和send,系统就会自动进行ip和端口号的绑定。
网络IP地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
基于udp套接字的在线翻译程序
sever
#include<map>
#include<sstream>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<iostream>
#include<string>
#include<cstdio>
using namespace std;
class SocketSever{
private:
//string ip;
int post;
int sock;//文件描述符,创建套接字返回值
map<string,string> dict;//字典
//pair<string,string> mp;
public:
SocketSever(int _post=8080):
post(_post)
{
dict.insert(pair<string,string>("苹果","apple"));
dict.insert(pair<string,string>("香蕉","banana"));
dict.insert(pair<string,string>("红色","red"));
dict.insert(pair<string,string>("黑色","block"));
dict.insert(pair<string,string>("绿色","green"));
dict.insert(pair<string,string>("蓝色","blue"));
}
void Init(){
sock=socket(AF_INET,SOCK_DGRAM,0);
cout<<"sock:"<<sock<<endl;
struct sockaddr_in in_addr;//IP地址和端口号在内核层面绑定
in_addr.sin_family=AF_INET;//设置协议家族一般为AF_INET IPV4
in_addr.sin_port=htons(post);//设置端口号,因为是网络通信所以要把主机序列转成网络序列
//in_addr.sin_addr.s_addr=inet_addr(ip.c_str());//设置IP地址,将字符串ip转成点分十进制的网络IP地址,因为string是一个类而inet_addr参数是一个字符串所以使用了c_str()转换。
in_addr.sin_addr.s_addr=INADDR_ANY;
if(bind(sock,(const struct sockaddr*)&in_addr,sizeof(in_addr))<0){
cout<<"绑定失败。。。"<<endl;
exit(1);
}
}
void star(){
char buff[64];
while(1){
struct sockaddr_in addr;
socklen_t len=sizeof(addr);//实际读到的大小
ssize_t size=recvfrom(sock,buff,sizeof(buff)-1,0,(struct sockaddr*)&addr,&len);//接受
if(size>0){
cout<<"接受成功"<<endl;
char buf[16];
sprintf(buf,"%d",ntohs(addr.sin_port));
string IP_PORT=inet_ntoa(addr.sin_addr);
IP_PORT+=" ";//to_string(ntohs(addr.sin_port).str());
IP_PORT+=buf;
cout<<"client‘s IP and port is"<<IP_PORT<<endl;
buff[size]=0;
//cout<<"client:"<<buff<<endl;
string str="";
if(dict.find(buff)!=dict.end()){
str=dict[buff];
}else{
str="no find";
}
//string str="sever 已经收到数据。。。";
size_t s=sendto(sock,str.c_str(),sizeof(str)-1,0,(struct sockaddr*)&addr,len);
}
}
}
~SocketSever(){
close(sock);
}
};
int main(int argc,char *argv[]){
SocketSever ss(atoi(argv[1]));
ss.Init();
ss.star();
return 0;
}
client
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<iostream>
#include<string>
using namespace std;
class Socketclient{
private:
string ip;
int post;
int sock;//文件描述符,创建套接字返回值
public:
//ip是对应client的IP地址和端口号
Socketclient(string ip_="127.0.0.1",int _post=8080):
ip(ip_),
post(_post)
{
}
//客户端不需要绑定只需创建套接字
void Init(){
sock=socket(AF_INET,SOCK_DGRAM,0);
cout<<"sock:"<<sock<<endl;
//struct sockaddr_in in_addr;//IP地址和端口号在内核层面绑定
//in_addr.sin_family=AF_INET;//设置协议家族一般为AF_INET IPV4
//in_addr.sin_port=htons(post);//设置端口号,因为是网络通信所以要把主机序列转成网络序列
//in_addr.sin_addr.s_addr=inet_addr(ip.c_str());//设置IP地址,将字符串ip转成点分十进制的网络IP地址,因为string是一个类而inet_addr参数是一个字符串所以使用了c_str()转换。
//if(bind(sock,(const struct sockaddr*)&in_addr,sizeof(in_addr))<0){
//cout<<"绑定失败。。。"<<endl;
//exit(1);
//}
}
//client是先收后发,client是先发后收
void star(){
//char buff[64];
string str;
while(1){
cout<<"请输入要发送的内容:";
cin>>str;
struct sockaddr_in in_addr;//IP地址和端口号在内核层面绑定
in_addr.sin_family=AF_INET;//设置协议家族一般为AF_INET IPV4
in_addr.sin_port=htons(post);//设置端口号,因为是网络通信所
in_addr.sin_addr.s_addr=inet_addr(ip.c_str());//设置IP地址,将字符串ip转成点分十进制的网络IP地址,因为string是一个类而inet_addr参数是一个字符串所以使用了c_str()转换。以要把主机序列转成网络序列
//
size_t s=sendto(sock,str.c_str(),sizeof(str)-1,0,(struct sockaddr*)&in_addr,sizeof(in_addr));
if(s<0){
cout<<"传输错误"<<endl;
}
char buff[256];
struct sockaddr_in addr;
socklen_t t=sizeof(addr);
ssize_t size=recvfrom(sock,buff,sizeof(buff)-1,0,(struct sockaddr*)&addr,&t);
if(size>0){
buff[size]=0;
cout<<"翻译结果:"<<buff<<endl;
}
}
//struct sockaddr_in addr;
//socklen_t t struct sockaddr*)&in_addr,sizeof(in_addr)en=sizeof(addr);//实际读到的大小
//ssize_t size=recvfrom(sock,buff,sizeof(buff)-1,0,(struct sockaddr*)&addr,&len);//接受
//if(size>0){
//buff[size]=0;
//cout<<"client:"<<buff<<endl;
//string str="client 已经收到数据。。。";
//size_t s=sendto(sock,str.c_str(),sizeof(str)-1,0,(struct sockaddr*)&addr,len);
//}
//}
}
~Socketclient(){
close(sock);
}
};
void up(string str){
cout<<str<<"请输入IP地址和端口号"<<endl;
}
int main(int argc,char *argv[]){
if(argc!=3){
up(argv[0]);
exit(1);
}
Socketclient ss(argv[1],atoi(argv[2]));
ss.Init();
ss.star();
return 0;
}