正文开始!
端口号(port)是传输层协议的内容:
之前在进程的学习中可以知道pid表示唯一一个进程;此处端口号也是表示一个进程,那么这两者有什么关系呢?
一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定.
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号.就是在描述"数据是谁发的,要发给谁";
在这里我们先对TCP(Transmission Control Protocol 传输控制协议)做一个简单的认识;
在这我们也是对UDP(User Dategram Protocol 用户数据报协议)做一个简单的认识;
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端和小端之分,网络数据流同样有大端和小端之分,那么如何定义数据网络数据流的地址呢?
为了使网络程序具有可移植性,使用同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下的库函数做网络字节序和主机字节序的转换.
//创建 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);
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4,IPv6.
然后各种不同网络协议的地址格式并不相同.
虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息,地址类型,端口号,IP地址.
in_addr用来表示一个IPv4的IP地址.其实就是一个32位整数.
基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr,sin_addr表示32位的IP地址.但是我们通常使用点分十进制的字符串表示IP地址,以下的函数可以在字符串表示和in_addr表示之间转换:
inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果,那么是否需要调用者手动释放呢?
在这里我们查看man手册
man手册上面说,inet_ntoa函数是把这个结果放到了静态存储区.这个时候不需要我们手动释放.
那么问题来了,如果我们多次调用这个函数,会有什么样的效果呢?
参考如下代码
#include
#include
#includeint 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,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题;
实现一个大小写转化的功能
makefile
.PHONY:all
all:udpClient udpServer
udpClient:udpClient.ccg++ -o $@ $^ -std=c++11 -lpthread
udpServer:udpServer.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -rf udpClient udpServer
Log.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const 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=NULLFILE* 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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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.根据命令行设置要访问的服务器IPstring server_ip = argv[1];uint16_t server_port = stoi(argv[2]);// 2.创建客户端// 2.1创建socketint 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);//发送消息给serversendto(sockfd, buffer.c_str(),buffer.size(), 0, (const struct sockaddr *)&server, len);//首次调用sendto函数的时候,我们client会自动bind自己的ip和port}close(sockfd);return 0;
}
udpServer.cc
#include
#include
#include
#include
#include
#include
#include
#include
#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_instruct sockaddr_in local; // local在用户栈上开辟的空间--->临时变量--->写入内核中bzero(&local, sizeof local); // memsetlocal.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->nlocal.sin_addr.s_addr = _ip.empty() ? htonl(INADDR_ANY) : inet_addr(_ip.c_str());// 2.2if (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);//拿到了对方的ipuint32_t peerPort=ntohs(peer.sin_port);//拿到了对方的portcheckOnlineUser(peerIp,peerPort,peer);//如果存在,什么都不做,如果不存在就添加//打印出客户端给服务器发送的消息logMessage(NOTICE, "[%s|%d]# %s",peerIp.c_str(),peerPort,inbuffer);for(int i=0;iif(isalpha(inbuffer[i])&&islower(inbuffer[i]))outbuffer[i]=toupper(inbuffer[i]);elseoutbuffer[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;//onlineuserstd::unordered_map _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;
}
#pragma warning(disable:4996)
#pragma comment(lib,"Ws2_32.lib")
#include
#include
#include
#include
#include
#include
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创建socketSOCKET 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);//发送消息给serversendto(sockfd, buffer.c_str(), \buffer.size(), 0, (const struct sockaddr*)&server, sizeof(server));//首次调用sendto函数的时候,我们client会自动bind自己的ip和port} closesocket(sockfd);WSACleanup();return 0;
}
中间的乱码问题可能是因为编码问题,但是其他的内容我们就可以直接的进行通讯.
(本章完!)