文章目录
- 理解源IP地址和目的IP地址
- 认识端口号
- 认识TCP/UDP协议
- 网络字节序
- socket编程接口
-
- [socket 常见API](#socket 常见API)
- sockaddr结构
理解源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址;
源IP即发送方的地址,目的IP即接受方的地址;
但只有IP是不够的,完不成通信;
IP只能确认我们将信息发到哪一台机器上,但具体发给哪一个进程是不确定的;
因此,还需要有一个其他的标识来区分出,要发给哪个进程;
认识端口号
端口号(port)是传输层协议的内容
-
端口号是一个2字节16位的整数
-
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
-
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
-
一个端口号只能被一个进程占用
IP表示目的地址
端口号(port)用来表示唯一一个服务的
IP + port可以标识全网唯一的服务
前面我们谈到PID表示唯一一个进程;此时端口号表示唯一一个进程;
它们之间有什么关系么?
一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定;
端口号是传输层到应用层寻找服务时需要使用的字段,而进程的PID用于操作系统管理不同的进程;
认识TCP/UDP协议
TCP协议
- 传输层协议
- 有连接:通信前需要先建立连接
- 可靠传输:TCP协议有一些措施来保证传输的可靠性
- 面向字节流
UDP协议
- 传输层协议
- 无连接:通信前不需要建立连接
- 不可靠传输:无措施来保证可靠性
- 面向数据报
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
调用以下库函数做网络字节序和主机字节序的转换
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回;
socket编程接口
socket 常见API
cpp
// 创建 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);
sockaddr结构
套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)。在进行跨网络通信时我们需要传递的端口号和IP地址,而本地通信则不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体,其中sockaddr_in结构体是用于跨网络通信的,而sockaddr_un结构体是用于本地通信的。
为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockeaddr结构体,该结构体与sockaddr_in和sockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族。
此时当我们在传递在传参时,就不用传入sockeaddr_in或sockeaddr_un这样的结构体,而统一传入sockeaddr这样的结构体。在设置参数时就可以通过设置协议家族这个字段,来表明我们是要进行网络通信还是本地通信,在这些API内部就可以提取sockeaddr结构头部的16位进行识别,进而得出我们是要进行网络通信还是本地通信,然后执行对应的操作。此时我们就通过通用sockaddr结构,将套接字网络通信和本地通信的参数类型进行了统一。
实际我们在进行网络通信时,定义的还是sockaddr_in这样的结构体,只不过在传参时需要将该结构体的地址类型进行强转为sockaddr。
UdpServer
cpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;
class UdpServer
{
public:
UdpServer(uint16_t port = defaultport)
: _port(port), _sockfd(defaultfd)
{
}
void Init()
{
// 1. 创建socket,就是创建了文件
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno));
exit(Socket_Err);
}
lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);
// 2. 绑定,指定网络信息
struct sockaddr_in local;
bzero(&local, sizeof(local)); // memset
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY; // 0
// 填写结构体
int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n != 0)
{
lg.LogMessage(Fatal, "bind errr, %d : %s\n", errno, strerror(errno));
exit(Bind_Err);
}
}
void Start()
{
// 服务器不退出
char buffer[defaultsize];
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
InetAddr addr(peer);
buffer[n] = 0;
std::cout << "[" << addr.PrintDebug() << "]# " << buffer << std::endl;
sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
}
}
}
~UdpServer()
{
}
private:
// std::string _ip;
uint16_t _port;
int _sockfd;
};
UdpClient
cpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建socket
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
std::cout << "create socket success: " << sock << std::endl;
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());
while (true)
{
// 我们要发的数据
std::string inbuffer;
std::cout << "Please Enter# ";
std::getline(std::cin, inbuffer);
// 我们要发给server
ssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&server, sizeof(server));
if(n > 0)
{
char buffer[1024];
//收消息
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
buffer[m] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
else
break;
}
else
break;
}
close(sock);
return 0;
}