目录
一.预备知识
1.1IP与MAC
1.2端口号
1.3TCP与UDP协议
2.4网络字节序
二.socket编程接口
2.1socket常见API
2.2sockaddr结构
3.UDP网络程序
3.1服务端
3.1.1服务端创建套接字
3.1.2绑定服务端
3.1.3recvfrom
3.2客户端
3.2.1客户端创建套接字
3.2.2客户端绑定
3.2.3sendto
3.3引入命令行参数
3.4多人网络聊天制作
3.4.1客户端
3.4.2服务端
3.5完整代码:
IP地址,用来标识主机
源IP地址:发送方主机的IP地址
目的IP地址:接受方主机的IP地址
MAC地址,用来标识网卡
源MAC地址:发送方主机的MAC
目的MAC地址:接收方主机的MAC
补充:数据基本都是跨局域网传送的,网络层封装的报头当中有IP与MAC,IP地址是不会改变的。
但数据跨网络到达另一个局域网时,其源MAC地址和目的MAC地址就会发生变化。
作用:用来标识一台计算机中的一个进程。
介绍下socket通信:
通过IP与MAC可找到公网上的唯一主机,通过端口号可找到那台主机上的对应的进程
通过IP与MAC已经可以完成数据的传输了,但本质其实是两台计算机的进程在进行通信,是跨网络的,而端口号就是来标识计算机中的进程,也分为源端口号,目的端口号。
- 源端口号: 发送方主机的服务进程绑定的端口号,保证接收方能够找到对应的服务
- 目的端口号: 接收方主机的服务进程绑定的端口号,保证发送方能够找到对应的服务
补充内容:
1.端口号是传输层协议的内容。
2.进程间的通信有共享内存,消息队列等,以及套接字(跨网络)
3.一个端口号只能被一个进程占用。一个进程可以绑定多个端口号,但是一个端口号不能被多个进程同时绑定。
4.进程ID(PID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念;而端口号(port)是用来标识需要对外进行网络数据请求的进程的唯一性的,它是属于网络的概念。
底层可采用哈希的方式,建立了端口号和进程PID或PCB之间的映射关系
TCP:要进行数据传输,二者要先进行连接
UDP:进行数据传输时,不需要进行连接,发送数据即可,但无法确定数据的可靠性
大小端介绍
大端:高位存放在低地址,低位存放在高地址
小端:低位存放在低地址,高位存放在高地址
巧计:低低是小端
网络上的字节流内存一样,也有大小端之分。
TCP/IP协议规定,网络数据流采用大端字节序
,不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。(发送数据时按内存地址从低到高的顺序发出,接受数据时是按内存地址从低到高的顺序保存)
使网络程序具有可移植性,调用以下库函数实现网络字节序和主机字节序之间的转换
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
创建套接字:(TCP/UDP,客户端+服务器)
int socket(int domain, int type, int protocol);
绑定端口号:(TCP/UDP,服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
监听套接字socket :(TCP,服务器)
int listen(int sockfd, int backlog);
接收请求:(TCP,服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
建立连接:(TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中struct sockaddr_in是用于网络通信,struct sockaddr_un是用于本地通信。sockaddr_in结构体存储了协议家族,端口号,IP等信息,网络通信时可以通过这个结构体把自己的信息发送给对方,也可以通过这个结构体获取对端的这些信息。
为了能让套接字的网络通信和本地通信能够使用同一套函数接口,于是出现了sockeaddr
结构体。其中这三个结构体的头部前16位是一样的,这个字段叫做协议家族。所以在用这个函数时可以统一传入sockeadder结构体,在设置参数时就可以通过设置协议家族这个字段,来表明我们是要进行网络通信还是本地通信。
补充内容:
1.IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
2.IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
3.socket API可以都用struct sockaddr* 类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4、IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
介绍:制作一个server服务端,用来提供各种服务。制作一个client客户端,可向服务端发送请求,服务端接受数据并处理后,再将数据返回给客户端。都将二者进行封装为一个类。
创建套接字函数:
int socket(int domain, int type, int protocol);
参数解释:
domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或AF_INET6(IPv6)。
type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM和SOCK_DGRAM,如果是UDP的网络通信,就填入SOCK_DGRAM,叫做用户数据报服务。如果是TCP的网络通信,就填入SOCK_STREAM,叫做流式套接字,提供的是流式服务。
protocol:创建套接字的协议类别。你可以指明为TCP或UDP。设置为0表示的就是默认,就会根据前两个参数自动推导出协议类型。
返回值:创建成功返回文件描述符,失败返回-1。
代码:
class UdpServer
{
public:
void init() //初始化
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
std::cout << "创建套接字成功" << sockfd_ << std::endl;
if (sockfd_ < 0)
{
cerr<< "socket error" << endl;
exit(-1);
}
}
private:
int sockfd_; // 文件描述符
};
补充内容:
1.我们是在应用层编写代码,其我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,我们在应用层调用的接口都是系统接口。
2.进程调用socket函数,会创建一个相对应文件结构体,同样是在当前进程的 task_struct 中的所指向的一个指针数组里填入那个文件结构体地址,建立关联,后将那个指针数组下标返回,就是文件描述符。
3.每一个文件结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等,但对于打开的“网络文件”来说,这里的文件缓冲区对应的是网卡。
当套接字创建成功后,对应的文件就被创建且已被当前进程管理了,当该文件实际还未与网络真正相关联起来。
bind函数:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解释:
sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:传入的addr结构体的长度。
返回值:绑定成功返回0,绑定失败返回-1。
详细介绍struct sockaddr_in结构体:
struct in_addr
,其实该结构体当中就只有一个成员,该成员就是一个32位的整数,IP地址实际就是存储在这个整数当中的如何去理解绑定?
绑定就是将IP地址和端口号对应的网络文件建立关联,可以改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法。
可在构造函数时将IP与端口号就初始化,
代码:
class UdpServer
{
public:
UdpServer(int port, std::string ip = "")
: ip_(ip), port_(port)
{
}
void init()
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
std::cout << "创建套接字成功" << sockfd_ << std::endl;
if (sockfd_ < 0)
{
exit(-1);
}
struct sockaddr_in local;
bzero(&local, sizeof(sockaddr)); //将该结构体内容清空
local.sin_family = AF_INET; //进行填充
local.sin_port = htons(port_); //主机转网络
local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 会自动进行h->n转换
int k = bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)); // 绑定
if (k == -1)
{
exit(-2);
}
std::cout << "绑定成功" << k << std::endl;
}
private:
int sockfd_; // 文件描述符
int port_; // 端口号
std::string ip_; // IP地址
};
详细介绍字符串IP与整数IP:
字符串IP:127.239.231.987 这种形式,也做基于字符串的点分十进制IP地址。
整数IP:IP地址在进行网络传输时所用的形式,用一个32位的整数来表示IP地址。
为何需要进行IP类型的转换?
若以字符串IP进行数据的发送,则需要15个字节的大小。若把字符串ip分为4部分,每个部分都是0-255,这只需要8个比特位,4部分就是32个比特位,也就是4字节。
如何进行转换?
可以运用位段的方式,2个变量共享一块空间。在一个结构体中定义一个32位比特位的整数,再定义一个结构体,运用位段的方式,如:
操作系统也提供了几个IP类型转换的函数:
用来获取客户端发送的数据
参数解释:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数解释:
sockfd:从该套接字获取信息
buf:缓冲区,把数据读取到该缓冲区中
len:一次读多少数据
flags:表示阻塞读取,一般写0
src_addr:一个输出型参数,获取到对端的信息,有端口号,IP等,方便后序我们对其进行响应
addrlen:输入输出型参数,传入一个想要读取对端src_addr的长度,最后返回实际读到的长度
补充:
recvfrom函数提供的参数是struct sockaddr*
类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*
类型进行强转。
代码:
void start()
{
while (true)
{
char inbuffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t size = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct
sockaddr *)&peer, &len);
if (size > 0)
{
inbuffer[size] = '\0';
int port = ntohs(peer.sin_port);
std::string ip = inet_ntoa(peer.sin_addr);
std::cout << ip << ":" << port << "# " << inbuffer << std::endl;
}
else
{
std::cerr << "recvfrom error" << std::endl;
}
}
}
class Upclient
{
public:
Upclient(std::string ip, int port)
: ip_(ip), port_(port)
{
}
void init()
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
if (sockfd_ < 0)
{
exit(-1);
}
std::cout << "创建套接字成功" << sockfd_ << std::endl;
}
private:
int sockfd_; // 文件描述符
int port_; // 端口号
std::string ip_; // IP地址
};
服务端是给别人提供服务的,当服务器启动后,许多的客户端就可以通过IP与端口号去找到对应服务端的那个进程,且服务端一直都在运行,随时可以被连接,所以IP与端口号不能随意改变。
客户端的端口号是不需要自己主动去绑定的,客户端可以随时退出,若绑定了一个端口,那么这个端口号就只能属于一个进程用,其它客户端就不能使用这个端口号了,其实用sendto这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号,不能自己主动去绑定。
用来向服务端发送请求
sendto函数:
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:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。addrlen:传入dest_addr结构体的长度。
返回值说明:成功返回实际写入的字节数,写入失败返回-1
补充:由于sendto函数提供的参数也是struct sockaddr*
类型的,在传入结构体地址时需要将struct sockaddr_in*
类型进行强转
代码:
void strat()
{
struct sockaddr_in server;//填入对端网络信息
bzero(&server, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(port_);
server.sin_addr.s_addr = inet_addr(ip_.c_str());
std::string buffer;
while (true)
{
std::cerr << "Please Enter# ";
std::getline(std::cin, buffer);
sendto(sockfd_, buffer.c_str(), buffer.size(), 0,
(const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函
//数的时候,我们的client会自动bind自己的ip和port
}
}
先把服务端与客户端完善。
服务端:
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
std::string ip = "127.0.0.1"; // 本地环回
int port = atoi(argv[1]);
UdpServer *p = new UdpServer(port, ip);
p->init();
p->start();
return 0;
}
客户端:
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[1] << "ip" << argv[2] << "port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
Upclient *c = new Upclient(server_ip, server_port);
c->init();
c->strat();
return 0;
}
结果:
介绍:按照前面的介绍,现在客户端可以向服务端发送数据,服务端也可以接受到数据。
改进:当客户端向服务端发送数据时,服务端把相关的客户端记录下来,并把收到的消息发给所有的客户端。同时,客户端也要可以接受服务端发送来的消息。这样就完成了一个多人聊天室。
运行时创建一个线程,让这个线程去接受服务端发送的数据,主线程可继续向服务端发送数据。
static void *_recvfrom(void *args)
{
while (true)
{
Upclient *up = (Upclient *)args;
char inbuffer[1024];
struct sockaddr_in tmp;
socklen_t leng = sizeof(tmp);
ssize_t size = recvfrom(up->sockfd_, inbuffer, sizeof(inbuffer) - 1, 0,
(struct sockaddr *)&tmp, &leng);
inbuffer[size]='\0';
std::cout << " " << inbuffer << std::endl;
}
}
void strat()
{
struct sockaddr_in server;
bzero(&server, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(port_);
server.sin_addr.s_addr = inet_addr(ip_.c_str());
std::string buffer;
while (true)
{
std::cerr << "Please Enter# ";
std::getline(std::cin, buffer);
// 发送消息给server
sendto(sockfd_, buffer.c_str(), buffer.size(), 0,
(const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函
//数的时候,我们的client会自动bind自己的ip和port
pthread_t tid;
pthread_create(&tid, nullptr, _recvfrom, (void *)this);
}
}
添加 std::unordered_map
void start()
{
while (true)
{
char inbuffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t size = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (size > 0)
{
inbuffer[size] = '\0';
int port = ntohs(peer.sin_port);
std::string ip = inet_ntoa(peer.sin_addr);
std::cout << ip << ":" << port << "# " << inbuffer << std::endl;
checkonline(ip, port, peer); //记录所有
sendmessage(ip,port,inbuffer);
}
else
{
std::cerr << "recvfrom error" << std::endl;
}
}
}
void checkonline(std::string ip, uint32_t port, struct sockaddr_in peer)
{
std::string key = ip;
key += ":";
key += std::to_string(port);
auto iter = users.find(key);
if (iter == users.end())
{
users.insert({key, peer});
}
}
void sendmessage(std::string ip, uint32_t port, std::string messgae)
{
std::string str(ip);
str += " ";
str += std::to_string(port);
str += " ";
str += messgae;
for (auto T : users)
{
sendto(sockfd_, str.c_str(), str.size(), 0, (const struct sockaddr *)&(T.second), sizeof(T.second));
}
}
结果:输出结果有点乱,可以把一个客户端收到的消息重定向到另一个文件中,这里不再演示。
服务端:
class UdpServer
{
public:
UdpServer(int port, std::string ip = "")
: ip_(ip), port_(port)
{
}
void init()
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
std::cout << "创建套接字成功" << sockfd_ << std::endl;
if (sockfd_ < 0)
{
exit(-1);
}
struct sockaddr_in local;
bzero(&local, sizeof(sockaddr));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 会自动进行h->n转换
int k = bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)); // 绑定
if (k == -1)
{
exit(-2);
}
std::cout << "绑定成功" << k << std::endl;
}
void start()
{
while (true)
{
char inbuffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t size = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (size > 0)
{
inbuffer[size] = '\0';
int port = ntohs(peer.sin_port);
std::string ip = inet_ntoa(peer.sin_addr);
std::cout << ip << ":" << port << "# " << inbuffer << std::endl;
checkonline(ip, port, peer);
sendmessage(ip,port,inbuffer);
}
else
{
std::cerr << "recvfrom error" << std::endl;
}
}
}
void checkonline(std::string ip, uint32_t port, struct sockaddr_in peer)
{
std::string key = ip;
key += ":";
key += std::to_string(port);
auto iter = users.find(key);
if (iter == users.end())
{
users.insert({key, peer});
}
else
{
}
}
void sendmessage(std::string ip, uint32_t port, char message[])
{
std::string str(ip);
str += " ";
str += std::to_string(port);
str += " ";
str += message;
for (auto T : users)
{
sendto(sockfd_, str.c_str(), str.size(), 0, (const struct sockaddr *)&(T.second), sizeof(T.second));
}
}
private:
int sockfd_; // 文件描述符
int port_; // 端口号
std::string ip_; // IP地址
std::unordered_map users;
};
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
std::string ip = "127.0.0.1"; // 本地环回
int port = atoi(argv[1]);
UdpServer *p = new UdpServer(port, ip);
p->init();
p->start();
return 0;
}
客户端:
class Upclient
{
public:
Upclient(std::string ip, int port)
: ip_(ip), port_(port)
{
}
void init()
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
std::cout << "创建套接字成功" << sockfd_ << std::endl;
if (sockfd_ < 0)
{
exit(-1);
}
}
static void *_recvfrom(void *args)
{
while (true)
{
Upclient *up = (Upclient *)args;
char inbuffer[1024];
struct sockaddr_in tmp;
socklen_t leng = sizeof(tmp);
ssize_t size = recvfrom(up->sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&tmp, &leng);
inbuffer[size]='\0';
std::cout << " " << inbuffer << std::endl;
}
}
void strat()
{
struct sockaddr_in server;
bzero(&server, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(port_);
server.sin_addr.s_addr = inet_addr(ip_.c_str());
std::string buffer;
while (true)
{
std::cerr << "Please Enter# ";
std::getline(std::cin, buffer);
// 发送消息给server
sendto(sockfd_, buffer.c_str(), buffer.size(), 0,
(const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
pthread_t tid;
pthread_create(&tid, nullptr, _recvfrom, (void *)this);
}
}
private:
int sockfd_; // 文件描述符
int port_; // 端口号
std::string ip_; // IP地址
};
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[1] << "ip" << argv[2] << "port" << std::endl;
return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
Upclient *c = new Upclient(server_ip, server_port);
c->init();
c->strat();
return 0;
}