socket套接字编程就是在网络程序中编写代码,通过一套套的接字接口实现网络程序的编写。
udp通信编程
udp是无连接、不可靠、面向数据报的传输层协议。
特点:支持多对多连接交互通信。尽最大努力交付,不保证可靠性,但速度最快
各端的操作流程:
服务端操作流程:
- 创建套接字端口:在内核中创建socket结构体,关联进程与网卡之间的联系
- 为套接字绑定地址信息:网络通信中的数据都必须带有源端IP、源端端口、对端IP、对端端口、协议。在内核创建的socket结构体中描述IP地址端口以及协议,(必须主动绑定,告诉客户端自己的地址信息,如果不绑定客户端就不知道该发往哪个服务端了)为了告诉操作系统发往哪个IP地址,哪个端口的数据是交给我来处理的
- 接收数据:告诉操作系统发往哪个地址和端口的数据应该交给我处理,操作系统一旦接收到发往这个地址和端口的数据,就会将这条数据放到对应的socket的接收缓冲区中,然后服务端从对应的socket的接收缓冲区中取出数据。
- 发送数据:将数据写入内核中的socket发送缓冲区中,操作系统选择合适的时候将数据封装发送出去
- 关闭套接字:释放资源
客户端操作流程:第2,3步与服务端不同
- 创建套接字:在内核中创建socket结构体,关联进程与网卡之间的联系
- 为套接字绑定地址信息:描述在内核中创建的socket结构体的源端地址信息;发送的数据中源端地址信息就是绑定的地址信息(不推荐主动绑定地址,降低端口冲突的概率,从而确保数据发送的安全性)
- 发送数据:将数据放到socket的发送缓冲区中,操作系统选择合适时候封装数据并发送数据。若socket发送数据的时候还没绑定地址,则操作系统会选择合适的地址进行绑定。
- 接收数据:将数据写入内核中的socket发送缓冲区中,操作系统选择合适的时候将数据封装发送出去
- 关闭套接字:释放资源
socket接口介绍
1、创建套接字int socket(int domain, int type, int protocol)
参数内容(domian:地址域(本地通信-AF_LOCAL
、IPv4-AF_INET
、IPv6-AF_INET6
等)确定本次socket通信使用哪种协议版本的地址结构,不同的协议版本有不同的地址结构;type:套接字类型(流式套接字-SOCK_STREAM
、数据报套接字-SOCK_DGRAM
等);protocol:协议类型(TCP-IPPROTO_TCP
、UDP-IPPROTO_UDP
) ,默认为0-流式默认TCP,数据报默认UDP)
返回值:文件描述符-非负整数, 套接字所有其他接口的操作句柄,失败返回-1
2、为套接字绑定地址信息int bind(int sockfd, struct sockaddr *addr, socklen_t len)
参数内容(sockfd:创建套接字返回的操作句柄;addr:要绑定的地址信息;len:要绑定的地址信息长度)
中间的参数结构体有很多种
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
struct sockaddr_in
{
sa_family sin_family;//地址域
in_port_t sin_port;//端口号
struct in_addr sin_addr;//IP地址
};
struct sockaddr_in6
{
sa_family sin_family;//地址域
in_port_t sin6_port;//端口号
uint32_t sin6_flowinfo;
struct in6_addr, sin6_addr;//IP地址
unit32_t sin6_scope_id;
}
struct sockaddr_un
{
__SOCKADDR_COMMON (sun_);
char sun_path[108];
};
不同的地址结构,有统一的一个信息:前两个字节是地址域类型,bind可以绑定不同的地址结构,为了实现接口统一,因此用户定义地址结构的时候,需要定义自己需要的地址结构,例如IPv4就使用struct sockaddr_in,但是进行绑定的时候,统一类型强转成为sockaddr*类型
简单解析bind接口的实现
bind(fd,addr, len)
{
if(addr->sa_family==AF_INET)
{
//绑定IPv4地址信息,这个结构体按照sockaddr_in进行解析
}
else if (addr->sa_family==AF_INET6)
{
//绑定IPv6地址信息,这个结构体按照sockaddr_in6进行解析
}
else if ...
}
3、接收数据,接收发送者地址便于回复ssize_t recvfrom(int sockfd, char *buf, int len, int flag, struct sockaddr *peer_addr, socklen *addrlen)
参数内容(sockfd:创建套接字返回的操作句柄;buf:一块缓冲区,用于接收从接收缓冲区中取出数据;len:想要接收的数据长度;flag:操作选项标志,默认为0,表示阻塞操作;peer_addr:发送方的地址信息;addrlen:想要获取的地址信息长度以及返回实际长度)
返回值:成功返回实际接收到的数据字节长度,失败返回-1
4、发送数据ssize_t sendto(int sockfd, char *data, int len, int flag, struct sickaddr *peer_addr, socklen_t addrlen)
参数内容(socket:socket操作句柄;data:要发送的数据地址;len:要发送数据长度;flag:默认为0,表示阻塞操作;peer_addr:接收方的地址信息;addrlen:地址信息长度)
返回值:成功返回实际发送的数据字节长度,失败返回-1
5、关闭套接字int close(int fd)
网络字节序的转换接口
uint32_t htonl(uint32_t hostlong)
主机字节序到网络字节序的转换
uint16_t htons(uint16_t hostshort)
uint32_t ntohl(uint32_t netlong)
网络字节序到主机字节序的转换
uint16_t ntohs(uint16_t netshort)
in_addr_t inet_addr(const char *cp)
将字符串的点分十进制IP地址转换成为网络字节序的整数IP地址
char *inet_ntoa(struct in_addr in)
将网络字节序的整数IP地址转化为字符串点分十进制IP地址
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size)
将网络字节序的整数IP地址转化为字符串IP地址-兼容IPv4和IPv6
int inet_pton(int af, const char *src, void *dst)
将字符串的IP地址转换成为网络字节序的整数IP地址-兼容IPv4和IPv6
udp客户服务端代码实现
使用c++封装UdpSocket类,实例化的每一个对象都是一个udp通信套接字,并且能够通过成员接口实现udp通信流程
udpsocket.hpp
//udpsocket.hpp
#include <cstdio>
#include <string>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
using namespace std;
class UdpSocket
{
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(const string &ip, uint16_t port)
{
//定义IPv4地址结构体
struct sockaddr_in addr;
//地址信息赋值
addr.sin_family = AF_INET;
addr.sin_port = htons(port);//将主机字节序短整型型数据转化为网络字节序数据
addr.sin_addr.s_addr = inet_addr(ip.c_str());//将字符串IP地址转化为网络字节序IP地址
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0)
{
perror("bind error");
return false;
}
return true;
}
//接收数据,获取发送端地址信息
bool Recv(string *buf, string *ip=NULL, uint16_t *port=NULL)
{
struct sockaddr_in peer_addr;//用于接收发送端的地址信息
socklen_t len = sizeof(struct sockaddr_in);
char tmp[4096] = {0};
int ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&peer_addr, &len);
if (ret < 0)
{
perror("recvfrom error");
return false;
}
buf->assign(tmp, ret);//assign从指定字符串中截取指定长度的数据到buf中
if (port != NULL)
{
*port = ntohs(peer_addr.sin_port);//网络字节序到主机字节序的转换
}
if (ip != NULL)
{
*ip = inet_ntoa(peer_addr.sin_addr);//网络字节序到字符串IP地址的转换
}
return true;
}
//发送数据
bool Send(const string &data, 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.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 Close()
{
if (_sockfd > 0)
{
close(_sockfd);
_sockfd = -1;
}
return true;
}
private:
int _sockfd;
};
udp_srv.cpp
#include <iostream>
#include <string>
#include "udpsocket.hpp"
using namespace std;
#define CHECK_RET(q) if((q)==false){return false;}
int main(int argc, char *argv[])
{
//运行时有三个参数 udp_src 192.168.73.29 4096
if (argc != 3)
{
cout << "Usage: ./udp_srv ip prot\n";
return -1;
}
uint16_t port = stoi(argv[2]);
string ip = argv[1];
UdpSocket srv_sock;
//创建套接字
CHECK_RET(srv_sock.Socket());
//绑定地址信息
CHECK_RET(srv_sock.Bind(ip, port));
while(1)
{
//接收数据
string buf;
string peer_ip;
uint16_t peer_port;
CHECK_RET(srv_sock.Recv(&buf, &peer_ip, &peer_port));
cout << "client["<<peer_ip<<":"<<peer_port<<"] say: " << buf << endl;
//发送数据
buf.clear();
cout << "server say: ";
cin >> buf;
CHECK_RET(srv_sock.Send(buf, peer_ip, peer_port));
}
//关闭套接字
srv_sock.Close();
return 0;
}
udp_cli.cpp
#include <iostream>
#include <string>
#include "udpsocket.hpp"
using namespace std;
#define CHECK_RET(q) if((q)==false){return -1;}
main(int argc, char *argv[])
{
if (argc != 3)
{
cout << "Usage: ./udpcli ip port\n";
return -1;
}
string srv_ip = argv[1];
uint16_t srv_port = stoi(argv[2]);
UdpSocket cli_sock;
//创建套接字
CHECK_RET(cli_sock.Socket());
//绑定数据(不推荐)
while(1)
{
//发送数据
cout << "client say:";
string buf;
cin >> buf;
CHECK_RET(cli_sock.Send(buf, srv_ip, srv_port));
//接收数据
buf.clear();
CHECK_RET(cli_sock.Recv(&buf));
cout << "server say: " << buf << endl;
}
//关闭套接字
cli_sock.Close();
return 0;
}
makefile
all:udp_srv udp_cli
udp_srv:udp_srv.cpp
g++ -std=c++11 $^ -o $@
udp_cli:udp_cli.cpp
g++ -std=c++11 $^ -o $@
运行结果:
通过ifconfig命令查看网卡信息
先运行服务端(ip地址必须为网卡信息中的IP地址)
再运行客户端,并对指定服务端发送数据
服务端收到数据并回复
客户端也收到数据
netstat
-查看网络状态信息
-a表示查看所有网络信息
-n表示查看原始ip地址信息
-p表示查看网络状态对应的进程
-t表示查看tcp的信息
-u表示查看udp的信息