文章目录
- 前言
- 一、理解源IP地址和目的IP地址
- 二、认识端口号
- 理解"端口号"和"进程ID"
- 理解源端口号和目的端口号
- 三、认识TCP协议
- 四、认识UDP协议
- 五、网络字节序
- 六、socket编程接口
- socket常见API
- sockaddr结构
- sockaddr结构
- sockaddr_in 结构
- in_addr结构
- 七、地址转换函数
- 关于inet_ntoa
- 八、简单的UDP网络程序
- 封装UDPSocket
- Windows下的客户端实现
- 总结
前言
- 认识IP地址,端口号,网络字节序等网络编程中的基本概念;
- 学习socket api的基本用法;
- 能够实现一个简单的UDP客户端/服务器;
- 能够实现一个简单的TCP客户端/服务器(单连接版本,多进程版本,多线程版本);
- 理解TCP服务器建立连接,发送数据,断开连接的流程.
正文开始!
一、理解源IP地址和目的IP地址
- 源IP地址:对应的就是标定通信主机的本机主机.
- 目的IP地址:对应的就是标定通信主机的目的主机.
二、认识端口号
端口号(port)是传输层协议的内容:
- 端口号是一个2字节16位的整数;
- 端口号用来表示一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
- IP地址+端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用;
理解"端口号"和"进程ID"
之前在进程的学习中可以知道pid表示唯一一个进程;此处端口号也是表示一个进程,那么这两者有什么关系呢?
一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定.
理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号.就是在描述"数据是谁发的,要发给谁";
三、认识TCP协议
在这里我们先对TCP(Transmission Control Protocol 传输控制协议)做一个简单的认识;
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
四、认识UDP协议
在这我们也是对UDP(User Dategram Protocol 用户数据报协议)做一个简单的认识;
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
五、网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端和小端之分,网络数据流同样有大端和小端之分,那么如何定义数据网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节一次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定:W昂罗数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据.
- 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可.
为了使网络程序具有可移植性,使用同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下的库函数做网络字节序和主机字节序的转换.
- 这些函数名很好记,h表示host,n表示network,l表示32为长整数,s表示16位短整数.
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送.
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动的返回.
六、socket编程接口
socket常见API
//创建 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结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4,IPv6.
然后各种不同网络协议的地址格式并不相同.
- IPv和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址.
- IPv4,IPv6地址类型分别定义为常数AF_INET,AF_INET6.这样只要取得某种sockaddr结构体的首地址,不需要直到具体是那种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API都可以用struct sockaddr*类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4,IPv6以及各种类型的sockaddr结构体指针作为参数;
sockaddr结构
sockaddr_in 结构
虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息,地址类型,端口号,IP地址.
in_addr结构
in_addr用来表示一个IPv4的IP地址.其实就是一个32位整数.
七、地址转换函数
基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr,sin_addr表示32位的IP地址.但是我们通常使用点分十进制的字符串表示IP地址,以下的函数可以在字符串表示和in_addr表示之间转换:
关于inet_ntoa
inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果,那么是否需要调用者手动释放呢?
在这里我们查看man手册
man手册上面说,inet_ntoa函数是把这个结果放到了静态存储区.这个时候不需要我们手动释放.
那么问题来了,如果我们多次调用这个函数,会有什么样的效果呢?
参考如下代码
#include<stdio.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr=0;
addr2.sin_addr.s_addr=0xffffffff;
char* ptr1 = inet_ntoa(addr1.sin_addr);
char* ptr2 = inet_ntoa(addr2.sin_addr);
printf("ptr1: %s,ptr2: %s\n",ptr1,ptr2);
return 0;
}
因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果.
思考:如果有多个线程调用 inet_ntoa,是否会出现异常情况呢?—>一定会的!
在APUE这本书中,明确提出 inet_ntoa 不是线程安全的函数;
在多线程的环境下,推荐使用 inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题;
八、简单的UDP网络程序
实现一个大小写转化的功能
封装UDPSocket
makefile
.PHONY:all
all:udpClient udpServer
udpClient:udpClient.cc
g++ -o $@ $^ -std=c++11 -lpthread
udpServer:udpServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf udpClient udpServer
Log.hpp
#pragma once
#include<cstdio>
#include<ctime>
#include<cstdarg>
#include<cassert>
#include<cstdlib>
#include<cstring>
#include<cerrno>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
const char* log_level[]={"DEBUG","NOTICE","WARINING","FATAL"};
//logMessage(DEBUG,"%d",10);
void logMessage(int level,const char* format,...)
{
assert(level>=DEBUG);
assert(level<=FATAL);
char logInfo[1024];
char* name=getenv("USER");
va_list ap; //ap--->char*
va_start(ap,format);
vsnprintf(logInfo,sizeof(logInfo)-1,format,ap);
va_end(ap); //ap=NULL
FILE* out=(level==FATAL)?stderr:stdout;
fprintf(out,"%s | %u | %s | %s\n",\
log_level[level],(unsigned int)time(nullptr),\
name==nullptr?"unknow":name,logInfo);
}
// char* s=format;
// while(s)
// {
// case: '%'
// if(*(s+1)=='d')int x=va_arg(ap,int);
// break;
// }
udpClient.cc
#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
using namespace std;
struct sockaddr_in server;
static void Usage(string proc)
{
printf("Usage\n\t%s server_ip server_port\n", proc.c_str());
}
void *recverAndPrint(void *args)
{
while (true)
{
int sockfd = *(int *)args;
char buffers[1024];
memset(buffers,0,sizeof buffers);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffers, sizeof(buffers) - 1, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffers[s] = 0;
cout << "server echo# " << buffers << endl;
}
}
}
// ./udpClient server_ip server_port
//如果一个客户端要连接server必须知道server对应的ip和port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 1.根据命令行设置要访问的服务器IP
string server_ip = argv[1];
uint16_t server_port = stoi(argv[2]);
// 2.创建客户端
// 2.1创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
assert(sockfd > 0);
// 2.2 client 需不需要bind???--->需要bind,但是不需要用户自己bind,而是OS自动给你bind
// 所谓的"不需要",指的是::不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!
//如果我非要自己bind呢? 可以! 严重不推荐!
// 所有的客户端软件 <->服务器 通信的时候,必须得有 client[ip,port] <->server[ip,port]
// 为什么呢? client很多,不能给客户端bind指定的port,port可能被别的client使用了,你的client就无法使用了
// 那么server凭什么要bind呢?server提供的服务,必须要被所有人知道!server不能随便改变!
// 2.2填写服务器对应的信息
bzero(&server, sizeof server);
socklen_t len = sizeof(server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
pthread_t t;
pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);
// 3.通讯过程
string buffer;
while (true)
{
cerr << "Please Enter# ";
getline(cin, buffer);
//发送消息给server
sendto(sockfd, buffer.c_str(),
buffer.size(), 0, (const struct sockaddr *)&server, len);
//首次调用sendto函数的时候,我们client会自动bind自己的ip和port
}
close(sockfd);
return 0;
}
udpServer.cc
#include <iostream>
#include<unordered_map>
#include <sys/types.h>
#include <strings.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
using namespace std;
//我们想写一个简单的udpServer
//云服务器有一些特殊情况
// 1.禁止你bind云服务器上的任何确定IP,只能使用INADDR_ANY,如果是虚拟机的话就可以!
class UdpServer
{
public:
UdpServer(uint16_t port, string ip = "")
: _sockfd(-1), _port(port), _ip(ip)
{
}
~UdpServer()
{
}
void init()
{
// 1.创建socket套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); //就是打开了一个文件
if (_sockfd < 0)
{
logMessage(FATAL, "socket:%s:%d", strerror(errno), _sockfd);
exit(1);
}
logMessage(DEBUG, "socket create success: %d", _sockfd);
// 2.绑定网络信息,知名ip+port
// 2.1先填充基本信息到 struct sockaddr_in
struct sockaddr_in local; // local在用户栈上开辟的空间--->临时变量--->写入内核中
bzero(&local, sizeof local); // memset
local.sin_family = AF_INET; // 填充协议家族,域
//填充服务器对应得端口号信息,一定是会发给对方的,_port一定回到网络中的
local.sin_port = htons(_port);
//服务器都必须具有IP地址,"81.70.251.220",字符串风格的点分十进制-->四字节IP-->uint32_t ip
// INADDR_ANY(0):程序员不关心会bind到哪一个ip,任意地址bind,强烈推荐的做法,所有服务器一般的做法
// inet_addr:指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行h->n
local.sin_addr.s_addr = _ip.empty() ? htonl(INADDR_ANY) : inet_addr(_ip.c_str());
// 2.2
if (bind(_sockfd, (const struct sockaddr *)&local, sizeof local) < 0)
{
logMessage(FATAL, "bind:%s:%d", strerror(errno), _sockfd);
exit(2);
}
logMessage(DEBUG, "socket bind success: %d", _sockfd);
// done
}
void start()
{
//服务器设计的时候,都是死循环
char inbuffer[1024]; //将来读取到的数据,都放在这里
char outbuffer[1024]; //将来发送到的数据,都放在这里
while (true)
{
struct sockaddr_in peer; //输出型参数
socklen_t len = sizeof(peer); //输入输出型参数
// UDP是无连接的
//对方给你发了消息,你想不想给对方回消息?要的!后面两个参数是输出型参数
ssize_t s = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (s > 0)
{
inbuffer[s] = '\0'; //当做字符串
}
else if (s == -1)
{
logMessage(WARINING, "recvfrom: %s:%d", strerror(errno), _sockfd);
continue;
}
//读取成功,除了读取到对方的数据,你还要读取到对方的网络地址[ip,port]
string peerIp=inet_ntoa(peer.sin_addr);//拿到了对方的ip
uint32_t peerPort=ntohs(peer.sin_port);//拿到了对方的port
checkOnlineUser(peerIp,peerPort,peer);//如果存在,什么都不做,如果不存在就添加
//打印出客户端给服务器发送的消息
logMessage(NOTICE, "[%s|%d]# %s",peerIp.c_str(),peerPort,inbuffer);
for(int i=0;i<strlen(inbuffer);i++)
{
if(isalpha(inbuffer[i])&&islower(inbuffer[i]))
outbuffer[i]=toupper(inbuffer[i]);
else
outbuffer[i]=inbuffer[i];
}
messageRoutine(peerIp,peerPort,inbuffer);//消息路由
//线程池!
//sendto(_sockfd,outbuffer,strlen(outbuffer),0,(const struct sockaddr*)&peer,sizeof(peer));
// logMessage(NOTICE,"server provide service succsee...");
// sleep(1);
}
}
void checkOnlineUser(string& ip,uint32_t port,struct sockaddr_in& peer)
{
string key=ip;
key+=":";
key+=to_string(port);
auto iter =_users.find(key);
if(iter==_users.end())
{
_users.insert({key,peer});
}
else
{
//do nothing
}
}
void messageRoutine(string ip,uint32_t port,string info)
{
string message="[";
message+=ip;
message+=":";
message+=to_string(port);
message+="]";
message+=info;
for(auto& e:_users)
{
sendto(_sockfd,message.c_str(),message.size(),0,(const struct sockaddr*)&(e.second),sizeof(e.second));
}
}
private:
//服务器socket fd信息
int _sockfd;
// 服务器必须得有端口号信息
uint16_t _port;
//服务器必须要有ip地址
string _ip;
//onlineuser
std::unordered_map<std::string,struct sockaddr_in> _users;
};
// struct ip
// {
// uint32_t part1:8;
// uint32_t part2:8;
// uint32_t part3:8;
// uint32_t part4:8;
// };
static void Usage(const string proc)
{
printf("Usage:\n\t%s port [ip]\n", proc.c_str());
}
// ./udpServer port [ip]
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = stoi(argv[1]);
string ip;
if (argc == 3)
{
ip = argv[2];
}
UdpServer svr(port, ip);
svr.init();
svr.start();
return 0;
}
Windows下的客户端实现
#pragma warning(disable:4996)
#pragma comment(lib,"Ws2_32.lib")
#include<iostream>
#include<cstdio>
#include<string>
#include<cassert>
#include<cstdlib>
#include<WinSock2.h>
using namespace std;
int main()
{
WSADATA data;
WSAStartup(MAKEWORD(2,2),&data);
string server_ip = "81.70.251.220";
uint16_t server_port = 8081;
// 2.创建客户端
// 2.1创建socket
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
assert(sockfd > 0);
// 2.2 client 需不需要bind???--->需要bind,但是不需要用户自己bind,而是OS自动给你bind
// 所谓的"不需要",指的是::不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!
//如果我非要自己bind呢? 可以! 严重不推荐!
// 所有的客户端软件 <->服务器 通信的时候,必须得有 client[ip,port] <->server[ip,port]
// 为什么呢? client很多,不能给客户端bind指定的port,port可能被别的client使用了,你的client就无法使用了
// 那么server凭什么要bind呢?server提供的服务,必须要被所有人知道!server不能随便改变!
// 2.2填写服务器对应的信息
struct sockaddr_in server;
memset(&server,'\0',sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
// 3.通讯过程
string buffer;
while (true)
{
cerr << "Please Enter# ";
getline(cin, buffer);
//发送消息给server
sendto(sockfd, buffer.c_str(), \
buffer.size(), 0, (const struct sockaddr*)&server, sizeof(server));
//首次调用sendto函数的时候,我们client会自动bind自己的ip和port
}
closesocket(sockfd);
WSACleanup();
return 0;
}
中间的乱码问题可能是因为编码问题,但是其他的内容我们就可以直接的进行通讯.
总结
(本章完!)