本文只介绍基于IPv4的socket网络编程
端口号
一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
认识UDP协议
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识, 后面再详细讨论.
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
套接字编程种类
- 域间套接字编程---同一机器内
- 原始套接字编程---网络工具
- 网络套接字编程---用户间的网络通信
我们这里讲网络套接字编程
socket 常见API
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,然而, 各种网络协议的地址格式并不相同.
Pv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,
不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
sockaddr 结构
sockaddr_in 结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.
in_addr结构
socket编程接口
socket
创建套接字,返回值是网络文件描述符 (TCP/UDP, 客户端 + 服务器)
参数:
domain: 创建套接字的域,我们这里填AF_INET表示使用ipv4的网络协议
type:套接字对应的类型,SOCK_STREAM字节流套接子,SOCK_DGRAM数据报套接字,就是TCP协议是面向字节流的,UDP协议是面向数据报的
protocol:协议类型,我们这里不用理,设为0先
返回值,就是网络文件描述符
bind
绑定套接字和端口号 (TCP/UDP, 服务器)
绑定的IP地址和端口号都在addr里,这个结构体需要我们自己设置,再传进去
伪代码:
struct sockaddr_in
{
sin_family;//创建套接字的域 AF_INET
sin_port;//端口号
sin_addr;//IP地址
};
recvfrom
接收发给套接字的网络信息
参数很好理解,flags设为0,后面的结构体是输出型参数,记录是谁发的
send
向套接字发送信息
地址转换函数
本文只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位的IP 地址但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示 和in_addr表示之间转换
头文件
#include <arpa/inet.h>
字符串转in_addr的函数
这个函数不仅把字符串转为32IP地址,还转为网络字节序了
in_addr转字符串的函数
简单的UDP网络程序
一个关于IP地址
当你使用的是云服务器,udp服务端绑定公网IP时,禁止绑定,因为我们看到的是虚拟的IP地址,一般绑定0.0.0.0的IP地址表示绑定任意地址的IP地址,凡是发给我服务端的数据,都要根据端口号向上交付。我们也不建议绑定固定地址,当
一个关于端口号port
端口号不是任意绑定的,[0 ,1023]:系统内定的端口号,一般都要有固定的应用层协议使用,比如http是80,https是443,mysql是3306,这个是例外。所以我们绑定端口号时,绑定1024+以上。
下面是udp服务端代码
- 创建套接字
- 绑定IP和端口号
- 接收消息
- 发送信息
udpserver.hpp
#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <unordered_map>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"// typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;std::string defaultip = "0.0.0.0";
uint16_t defaultport = 8888;
const int size = 1024;Log log;
enum
{SOCKET_ERR = 1,BIND_ERR,
};class UdpServer
{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) : ip_(ip), port_(port), isrunning(false){}void Init(){// 1.创建udp socket// Udp 的socket是全双工的,允许被同时读写的socketfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (socketfd_ < 0){log(Fatal, "create socket erro,socketfd:%d", socketfd_);exit(SOCKET_ERR);}log(Info, "socket create success, socketfd: %d", socketfd_);// 2.绑定socketstruct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_); // 需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 1. string -> uint32_t 2. uint32_t必须是网络序列的if (bind(socketfd_, (struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}log(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));}void ChectUser(const struct sockaddr_in &client, uint16_t clientport, const std::string &clientip){auto it = online_user_.find(clientip);if (it == online_user_.end()){online_user_.insert({clientip, client});std::cout << "[" << clientip << ":" << clientport << "]" << "add to online user" << std::endl;}}void BroadCast(const std::string &info, uint16_t clientport, const std::string &clientip){for (const auto &users : online_user_){std::string massege = "[";massege += clientip;massege += ":";massege += to_string(clientport);massege += "]# ";massege += info;socklen_t len = sizeof(users.second);sendto(socketfd_, massege.c_str(), massege.size(), 0, (struct sockaddr *)&users.second, len);}}void Run(){isrunning = true;char buff[size];while (true){memset(buff,0,size);struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(socketfd_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&client, &len);if (n < 0){log(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);ChectUser(client, clientport, clientip);std::string info = buff;BroadCast(info, clientport, clientip);}}~UdpServer(){}private:int socketfd_; // 网络文件描述符std::string ip_; // 绑定的IP地址uint16_t port_; // 绑定的端口号bool isrunning;std::unordered_map<std::string, struct sockaddr_in> online_user_; // 存在线用户
};
main.cc 运行服务端
#include "udpserver.hpp"
#include <memory>
#include <cstdio>
#include <vector>
#include <functional>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);// std::unique_ptr<UdpServer> svr(new UdpServer(port));UdpServer *svr = new UdpServer(port);svr->Init();svr->Run();return 0;
}
udpclient.cc 客户端
1. 创建套接字
2. client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
系统什么时候给我bind呢?首次发送数据的时候
3.创建两个线程分别进行收发数据
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>void Usage(const std::string proc)
{std::cout << "\n\tUsage: " << proc << " serverip serverport\n"<< std::endl;
}struct ThreadData
{std::string serverip;struct sockaddr_in server;int sockfd;
};void *Recv_massege(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);char buff[1024];while (true){memset(buff, 0, sizeof(buff));struct sockaddr_in tmp;socklen_t len = sizeof(tmp);ssize_t n = recvfrom(td->sockfd, buff, sizeof(buff), 0, (struct sockaddr *)(&tmp), &len);if (n > 0){buff[n] = 0;std::cerr<< buff << std::endl;}}
}void *Send_massege(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);std::string massege;socklen_t len = sizeof(td->server);while (true){std::cout << "Please Enter@ ";getline(std::cin, massege);// 1. 数据 2. 给谁发sendto(td->sockfd, massege.c_str(), massege.size(), 0, (struct sockaddr *)&(td->server), len);}
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);ThreadData td;bzero(&td.server, sizeof(td.server));td.server.sin_addr.s_addr = inet_addr(serverip.c_str());td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport);td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (td.sockfd < 0){std::cout << "sock erro" << std::endl;exit(1);}td.serverip = serverip;// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!// 系统什么时候给我bind呢?首次发送数据的时候// 创建两个线程,分别收发数据pthread_t recv_tid, send_tid;pthread_create(&recv_tid, nullptr, Recv_massege, &td);pthread_create(&send_tid, nullptr, Send_massege, &td);pthread_join(recv_tid, nullptr);pthread_join(send_tid, nullptr);return 0;
}