网络编程套接字
udp传输
客户端
1、创建套接字 socket()
2、为套接字绑定地址 bind()
3、发送数据(如果socket还没有绑定地址,这时候操作系统会选择一个合适的地址端口进行绑定)
4、接收数据
5、关闭套接字
服务端
1、创建套接字,通过创建套接字使进程与网卡建立联系,创建struct socket{…}
2、为套接字绑定地址信息
3、接收数据
4、发送数据
5、关闭套接字
创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domin: 地址域
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
type:套接字类型
SOCK_STREAM 流式套接字,默认协议TCP,不支持UDP
SOCK_DGRAM 数据报套接字,默认协议UDP,不支持TCP
protocol:协议类型
0:使用默认套接字协议
6/IPPOTO_TCP TCP协议
17/IPPOTO_UDP UDP协议
返回值:套接字操作句柄-文件描述符 失败返回-1
为套接字绑定地址
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
bind()函数应将本地套接字地址地址分配给由描述符套接字标识的套接字,该套接字没有分配本地套接字地址.使用socket()函数创建的套接字最初是未命名的;它们仅由其地址族标识。
socket:套接字文件描述符
sockaddr:地址信息
addrlen:地址信息长度
返回值:0 失败返回-1
详细了解地址家族
在bind函数中我们看到了sockaddr这个关键的地址家族。通过man手册只能找到少量的描述。通过查找资料找到了一些,再此做出总结
一般我们使用struct sockaddr和struct sockaddr_in这两个结构体用来处理网络通信的地址。
struct sockaddr
{
unsigned short sa_family; //2
char sa_data[14]; //14
};
该结构体的缺陷为sa_data[]把目标地址和端口信息混在一起了
上面是通用的socket地址,具体到Internet socket,用下面的结构,二者可以进行类型转换
struct sockaddr_in
{
sa_family_t sin_family;//地址族
uint16_t sin_port;//16位TCP/UCP端口号
struct in_addr sin_addr;//32位IP地址
char sin_zero[8];//不使用
}
在这个结构体中存在另一个结构体,该结构体一般用来存放32位IP地址
struct in_addr
{
In_addr_t s_addr;//32位IPV4地址
}
sin_port和sin_addr都必须是网络字节序(NBO),一般可视化的数字都是主机字节序(HBO)。
一般用法
int sockfd;
struct sockaddr_in my_addr; //赋值时用这个结构
sockfd = socket(AF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT);
my_addr.sin_addr.s_addr = inet_addr("192.168.0.1");
bzero(&(my_addr.sin_zero), 8);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
//用(struct sockaddr *)转换即满足要求
还有网络上另一个其他解释
struct sockaddr 是一个通用地址结构,这是为了统一地址结构的表示方法,统一接口函数,使不同的地址结构可以被bind() , connect() 等函数调用;struct sockaddr_in中的in 表示internet,就是网络地址,这只是我们比较常用的地址结构,属于AF_INET地址族,他非常的常用,以至于我们都开始讨论它与 struct sockaddr通用地址结构的区别。
接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
sockfd:套接字文件描述符
buf:用buf存储接收的数据
len:想要接收的数据长度
flags:0---默认阻塞接收
src_addr:发送端地址信息
addrlen:地址信息长度,不但要指定想接收多长,还要保存实际接受了多长
返回值:实际接收的长度 失败返回-1
发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd:套接字文件描述符
buf:要发送的数据
len:要发送的数据长度
flags:0---默认阻塞发送
dest_addr:目的段地址信息--标识数据要发送到哪里去
addrlen:地址信息长度
返回值:实际发送的长度 失败返回-1
关闭套接字
int close(int fd);
这个在Linux文件操作符中已经了解到了,文件描述符用完之后需要被关闭
UDP传输Demo
//UdpSocket.hpp
//实现以UdpSocket类封装udp常用操作
#include <iostream>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
class UdpSocket{
public:
UdpSocket():_sock(-1){}
~UdpSocket(){}
bool Socket() {
_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (_sock < 0) {
perror("socket error");
return false;
}
return true;
}
bool Bind(string &ip, uint16_t port) {
struct sockaddr_in addr;//定义一个结构体名称为addr,是socket地址
//in 表示internet,就是网络地址
//sin_family指代协议族,在socket编程中只能是AF_INET
//sin_port存储端口号(使用网络字节顺序)
//sin_addr存储IP地址,使用in_addr这个数据结构
addr.sin_family = AF_INET;//ipv4
//uint16_t htons(uint16_t hostshort);
addr.sin_port = htons(port);//htons是将整型变量从主机字节顺序转变成网络字节顺序,
//就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
//in_addr_t inet_addr(const char *cp);
addr.sin_addr.s_addr = inet_addr(ip.c_str());//inet_addr是一个计算机函数,
//addr.sin_addr.s_addr = INADDR_ANY;
// 功能是将一个点分十进制的IP转换成一个长整数型数(u_long类型)等同于inet_addr()。
socklen_t len = sizeof(struct sockaddr_in);
//int bind(int sockfd, struct sockaddr *addr,
// socklen_t addrlen);
int ret = bind(_sock, (struct sockaddr*)&addr, len);
if (ret < 0) {
perror("bind error");
return false;
}
return true;
}
bool Recv(string &buf, struct sockaddr_in *saddr) {
//ssize_t recvfrom(int sockfd, void *buf, size_t len,
//int flags, struct sockaddr *src_addr, socklen_t *addrlen);
char tmp[1500] = {0};
socklen_t len = sizeof(struct sockaddr_in);
int ret = recvfrom(_sock, tmp, 1500, 0,
(struct sockaddr*)saddr, &len);
if (ret < 0) {
perror("recvfrom error");
return false;
}
buf.assign(tmp, ret);
return true;
}
bool Send(string &buf, struct sockaddr_in *daddr){
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(_sock, buf.c_str(), buf.size(), 0,
(struct sockaddr*)daddr, len);
if (ret < 0) {
perror("sendto error");
return false;
}
return true;
}
bool Close() {
close(_sock);
_sock = -1;
}
private:
int _sock;
};
在这之外,上面的头文件中还有几个其他接口函数,需要了解
htons()作用是将端口号由主机字节序转换为网络字节序的整数值。(host to net)
inet_addr()作用是将一个IP字符串转化为一个网络字节序的整数值,用于sockaddr_in.sin_addr.s_addr。
数字的字节序转换:
主机字节序转换为网络字节序
uint32_t htonl(uint32_t hostlong)
uint16_t htons(uint16_t hostshort)
网络字节序转换为主机字节序
uint32_t ntohl(uint32_t hostlong)
uint16_t ntohs(uint16_t hostshort)
将点分十进制ip地址转换为网络字节序ip地址:
in_addr inet_addr(const char *cp);
int inet_pton(int af,const char *src,void *dst)
将网络字节序IP地址转换为点分十进制IP地址:
const char *inet_ntop(int af,const void* src,char *dst,socklen_t size);
char *inet_ntoa(struct in_addr in);
udp服务端
#include "udpsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}
int main(int argc,char *argv[])
{
if(argc != 3)
{
cout<<"./udp_server ip port:"<<endl;
return -1;
}
string ip = argv[1];
uint16_t port = atoi(argv[2]);
UdpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Bind(ip,port));
while(1)
{
string buf;
struct sockaddr_in cli_addr;
CHECK_RET(sock.Recv(buf,&cli_addr));
cout<<"client say:"<<buf<<endl;
cout<<"server say:";
fflush(stdout);
cin>>buf;
CHECK_RET(sock.Send(buf,&cli_addr));
}
sock.Close();
}
udp客户端
#include "udpsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}
int main(int argc,char *argv[])
{
if(argc != 3)
{
cout<<"./udp_client ip port"<<endl;
return -1;
}
string ip = argv[1];
uint16_t port = atoi(argv[2]);
UdpSocket sock;
CHECK_RET(sock.Socket());
struct sockaddr_in srv_addr;
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(port);
srv_addr.sin_addr.s_addr = inet_addr(ip.c_str());
while(1)
{
string buf;
cout<<"client say:";
fflush(stdout);
cin>>buf;
CHECK_RET(sock.Send(buf,&srv_addr));
CHECK_RET(sock.Recv(buf,&srv_addr));
cout<<"server say"<<buf<<endl;
}
sock.Close();
}
两边聊天如上图
udp的传输速度快,无连接,不可靠,面向数据报这个必须牢牢记住了。
但是在代码中有个bug存在,发送的数据如果有空格,那么数据就会产生截断,分成两次发送
PS:2019.8.19更新,截断是因为cin或者scanf输入时遇到空格就停止,所以用getline(cin,buf)可以解决这个bug