目录
udp_echo_server:简单的回显服务器和客户端代码
套接字
套接字是实现进程间通信的编程。IP可以标定主机在全网的唯一性,端口可以标定进程在主机的唯一性,那么socket通过IP+端口号就可以让两个在全网唯一标定的进程进行通信。
套接字分为:
|-------|---------------------------------------|
| 域间套接字 | 实现主机内部的进程通信的编程 |
| 原始套接字 | 使用网络层或者数据链路层的接口进行编程,更难更底层,例如制作抓包等网络工具 |
| 网络套接字 | 实现用户通信的编程 |
socket编程
源IP地址和目的IP地址
IP 在网络中, 用来标识主机的唯一性;
源IP地址是发送数据的设备的IP地址,目的IP地址是接收数据的设备IP地址;
端口号
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用**。**
端口号范围划分
- 0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的.
- 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的
既然端口号用来标识一个进程,那我们之前学习的PID也是能够标识唯一进程,那为什么还有有端口号呢?
进程PID是系统概念,技术上也是唯一性,确实可以标识唯一进程,但是这样做,会使得系统进程关联和网络强耦合,所以才会引进端口号这个概念;
port和pid的关系是什么?
系统中所有进程都有PID,但是并不是所有进程都要进行网络通信,只有进程网络通信的进程才有port;(可以类比学生号和身份证号来理解)
传输层协议(TCP 和 UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号.
就是在描述 "数据是谁发的, 要发给谁";
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
socket常用API
// 创建 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);
socket结构
- IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括 16 位地址类型, 16 位端口号和 32 位 IP 地址.
- IPv4、 IPv6 地址类型分别定义为常数 AF_INET、 AF_INET6. 这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容.
struct sockaddr_in结构:
虽然 socket api 的接口是 sockaddr, 但是我们真正在基于 IPv4 编程时, 使用的数据结构是 sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP 地址
UDP
UDP(UserDatagramProtocol)是一个简单的面向消息的传输层协议,尽管UDP提供标头和有效负载的完整性验证(通过校验和),但它不保证向上层协议提供消息传递,并且UDP层在发送后不会保留UDP 消息的状态。因此,UDP有时被称为不可靠的数据报协议。如果需要传输可靠性,则必须在用户应用程序中实现。
UDP协议(用户数据报协议)
传输层协议
无连接
不可靠传输(简单,但丢包问题解决不了)
面向数据报
创建套接字
_sockfd =::socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd<0)
{
LOG(FATAL,"socket error\n");
exit(SOCKET_ERROR);
}
LOG(DEBUG,"socket success,_socket:%d\n",_sockfd);
绑定
struct sockaddr_in local;
memset(&local,0,sizeof(local));//清零
local.sin_family=AF_INET;
local.sin_port = htons(_localport);//端口号,主机序列转网络序列
// local.sin_addr.s_addr=inet_addr(_localip.c_str());//ip地址,1、4字节,2、需要网络序列的IP
local.sin_addr.s_addr=INADDR_ANY;//INADDR_ANY 服务器端进行任意IP地址绑定
int n=::bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n<0)
{
LOG(FATAL,"bind error\n");
exit(BIND_ERROR);
}
LOG(DEBUG,"socket bind success\n");
通信
完成上面的准备工作后,我们就可以进行UDP服务器通信了;
因为UDP是面向数据报,所以这里我们使用recvfrom和sendto函数来进行接收信息和发送信息;
recvfrom
因为我们这里接收到的是网络序列,如果想要把他打印出来,就必须转换程主机序列才行;
struct sockaddr_in peer;
socklen_t len =sizeof(peer);
ssize_t n=recvfrom(_sockfd,inbuff,sizeof(inbuff)-1,0,(struct sockaddr*)&peer,&len);
sendto
InetAddr addr(peer);
inbuff[n]=0;
cout<<"["<<addr.Ip()<<":"<<addr.Port()<<"]# "<<inbuff<<endl;
string echo_string ="[udp_server echo]#";
echo_string+=inbuff;
//返回客户端
sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&peer,len);
这里我们在InetAddr中进行了转换;
这样我们就完成了服务端的代码;
接下来我们将完善客户端的代码,同样也是要创建套接字和绑定;和服务端不同的是:
client 的端口号,一般不让用户自己设定,而是让client OS随机选择;怎么选择?什么时候选择?
client 需要绑定他自己的ip和端口,但是client 不需要显示绑定他自己的ip和端口;
client在首次向服务器发送数据的时候,OS会自动给client bind 他自己的IP和端口;
所以绑定:
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
绑定完直接通信即可;
udp_echo_server:简单的回显服务器和客户端代码
代码:
dict_server:简单的英译汉的网络字典
代码:
这个字典只需要在udp_echo_server的基础上修改即可;
在服务端利用回调函数,将获取到的消息,在字典中查询,如果可以找到就返回它的翻译,如果找不到就返回None;
然后我们将翻译函数封装,绑定:
绑定就是在main.cpp中
以上就是服务端的内容;
客户端不需要改变;因为这个英译汉本身就是在收到客户端的信息后进行翻译,客户端不需要其他的处理;
chat_server:简单聊天室
代码:
在udp_echo_server基础上修改,服务器收到信息,将信息转交给其他人,然后继续读取信息;对应信息的发送,我们单独封装到一个Route.hpp里面;
如何多线程来实现?
以上就是UDP socket