文章目录
一、预备知识
1.IP和端口号
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
端口号(port)是传输层协议的内容.
端口号是一个2字节16位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
一个端口号只能被一个进程占用
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要
发给谁";
2.TCP协议和UDP协议
TCP(Transmission Control Protocol 传输控制协议):传输层协议 有连接 可靠传输 面向字节流
UDP(User Datagram Protocol 用户数据报协议):无连接 不可靠传输 面向数据报
3.网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏
移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
#include<arpa/inet.h>// 主机转网络uint32_thtonl(uint32_t hostlong);uint16_thtons(uint16_t hostshort);//网络转主机uint32_tntosl(uint32_t netlong);uint16_tntohs(uint16_t netshort);
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
二、socket编程接口
1.socket 常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)intsocket(int domain,int type,int protocol);// 绑定端口号 (TCP/UDP, 服务器) intbind(int socket,conststructsockaddr*address,socklen_t address_len);// 开始监听socket (TCP, 服务器)intlisten(int socket,int backlog);// 接收请求 (TCP, 服务器)intaccept(int socket,structsockaddr* address,socklen_t* address_len);// 建立连接 (TCP, 客户端)intconnect(int sockfd,conststructsockaddr*addr,socklen_t addrlen);
2.sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain
Socket. 然而, 各种网络协议的地址格式并不相同
IPv4和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, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
sockaddr 结构
sockaddr_in 结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.
in_addr结构
地址转换函数
IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
intinet_aton(constchar*cp,structin_addr*inp);
in_addr转字符串的函数:
char*inet_ntoa(structin_addr in);
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。
#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>intinet_aton(constchar*cp,structin_addr*inp);in_addr_tinet_addr(constchar*cp);in_addr_tinet_network(constchar*cp);char*inet_ntoa(structin_addr in);structin_addrinet_makeaddr(int net,int host);in_addr_tinet_lnaof(structin_addr in);in_addr_tinet_netof(structin_addr in);
使用案例:
#include<cstdio>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>intmain(){structsockaddr_in addr;// 字符串转in_addr的函数:inet_aton("127.0.0.1",&addr.sin_addr);uint32_t*ptr =(uint32_t*)(&addr.sin_addr);printf("addr:%x\n",*ptr);// in_addr转字符串的函数:printf("addr_str: %s\n",inet_ntoa(addr.sin_addr));return0;}
关于inet_ntoa
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是
否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码
#include<cstdio>#include<netinet/in.h>#include<arpa/inet.h>intmain(){structsockaddr_in addr1;structsockaddr_in addr2;
addr1.sin_addr.s_addr =0;
addr1.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);return0;}
运行结果如下:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.
思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
在APUE中, 明确提出inet_ntoa不是线程安全的函数;
但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;
多线程调用inet_ntoa代码示例如下:
#include<cstdio>#include<unistd.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<pthread.h>void*Func1(void*p){structsockaddr_in*addr =(structsockaddr_in*)p;while(1){sleep(1);char*ptr =inet_ntoa(addr->sin_addr);printf("addr1: %s\n", ptr);}returnNULL;}void*Func2(void*p){structsockaddr_in*addr =(structsockaddr_in*)p;while(1){sleep(1);char*ptr =inet_ntoa(addr->sin_addr);printf("addr2: %s\n", ptr);}returnNULL;}intmain(){
pthread_t tid1 =0;structsockaddr_in addr1;structsockaddr_in addr2;
addr1.sin_addr.s_addr =0;
addr2.sin_addr.s_addr =0xffffffff;pthread_create(&tid1,NULL, Func1,&addr1);
pthread_t tid2 =0;pthread_create(&tid2,NULL, Func2,&addr2);pthread_join(tid1,NULL);pthread_join(tid2,NULL);return0;}
这里暂时没有出现问题。
三、UDP服务器
以下使用UDP协议来实现处理三个不同的业务服务器:
1.英汉翻译服务器
2.执行shell指令的服务器
3.在线聊天的服务器
相关重要接口介绍
sendto
#include<sys/types.h>#include<sys/socket.h>ssize_tsendto(int sockfd,constvoid*buf,size_t len,int flags,conststructsockaddr*dest_addr,socklen_t addrlen);// sockfd:文件描述符// buf:发送缓冲区// len:缓冲区的大小// flags一般被置为0// dest_addr:接收端的网络协议的地址// addrlen:协议地址的长度
recvfrom
#include<sys/types.h>#include<sys/socket.h>ssize_trecvfrom(int sockfd,void*buf,size_t len,int flags,structsockaddr*src_addr,socklen_t*addrlen);// sockfd:文件描述符// buf:发送缓冲区// len:缓冲区的大小// flags一般被置为0// dest_addr:发送端的网络协议的地址// addrlen:协议地址的长度
popen
#include<stdio.h>
FILE *popen(constchar*command,constchar*type);intpclose(FILE *stream);// 函数功能:将command指令让shell执行,然后将结果返回到一个文件中// 返回值:保存结果的文件的地址// command参数指定要执行的shell命令// type参数指定管道的类型,可以是"r"或"w",分别表示读或写。
1.udpServer.hpp
#pragmaonce#include<iostream>#include<string>#include<cstring>#include<cerrno>#include<cstdlib>#include<functional>#include<strings.h>#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>namespace server
{usingnamespace std;conststatic string defaultIp ="0.0.0.0";conststaticint gnum =1024;typedef function<void(int, string,uint16_t, string)> func_t;enum{
SOCK_ERR =2,
BIND_ERR,
USAGE_ERR,
OPEN_ERR
};classudpServer{public:udpServer(const func_t &callback,constuint16_t&port,const string &ip = defaultIp):_callback(callback),_port(port),_ip(ip),_sockfd(-1){}voidiniteServer(){// 1.创建socket
_sockfd =socket(AF_INET, SOCK_DGRAM,0);if(_sockfd ==-1){
cerr <<"socket error:"<< errno <<":"<<strerror(errno)<< endl;exit(SOCK_ERR);}
cout <<"socket sucess"<<":"<< _sockfd << endl;// 2. 绑定port,ip(TODO)// 未来服务器要明确的port,不能随意改变structsockaddr_in local;// 定义了一个变量,栈,用户bzero(&local,sizeof(local));// 初始化
local.sin_family = AF_INET;
local.sin_port =htons(_port);// 你如果要给别人发消息,你的port和ip要不要发送给对方// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->uint32_t 2. htonl(); -> inet_addr
local.sin_addr.s_addr =htonl(INADDR_ANY);// 任意地址bind,服务器的真实写法int n =bind(_sockfd,(structsockaddr*)&local,sizeof(local));if(n ==-1){
cerr <<"bind error:"<< errno <<":"<<strerror(errno)<< endl;exit(BIND_ERR);}
cout <<"bind sucess"<< endl;// UDP Server 的预备工作完成}voidstart(){// 服务器的本质其实就是一个死循环char buffer[gnum];for(;;){// 读取数据structsockaddr_in peer;
socklen_t len =sizeof(peer);// 必填
ssize_t s =recvfrom(_sockfd, buffer,sizeof(buffer)-1,0,(structsockaddr*)&peer,&len);// 1. 数据是什么 2. 谁发的?if(s >0){
buffer[s]=0;
string clientip =inet_ntoa(peer.sin_addr);// 1. 网络序列 2. int->点分十进制IPuint16_t clientport =ntohs(peer.sin_port);
string message = buffer;
cout << clientip <<"["<< clientport <<"]"<< message << endl;// 我们只把数据读上来就完了吗?对数据做处理_callback(_sockfd, clientip, clientport, message);}}}~udpServer(){}private:uint16_t _port;
string _ip;// 实际上,一款网络服务器,不建议指明一个IPint _sockfd;
func_t _callback;// 回调};}
2.udpServer.cc
#include"udpServer.hpp"#include"onlineUser.hpp"#include<iostream>#include<memory>#include<string>#include<fstream>#include<tuple>#include<unordered_map>#include<signal.h>usingnamespace std;usingnamespace server;const string dictTxt ="./dict.txt";
unordered_map<string, string> dict;staticvoidUsage(const string &proc){
cout <<"\nUsage:\n\t"<< proc <<"local_port\n\n";}staticboolcutString(const string &target, string *s1, string *s2,const string &sep){// apple:苹果auto pos = target.find(sep);if(pos == string::npos)returnfalse;*s1 = target.substr(0, pos);*s2 = target.substr(pos + sep.size());returntrue;}staticvoidinitDict(){
ifstream in(dictTxt, ios::binary);if(!in.is_open()){
cerr <<"open file"<< dictTxt <<"error"<< endl;exit(OPEN_ERR);}
string line, key, value;while(getline(in, line)){if(cutString(line,&key,&value,":")){
dict.insert(make_pair(key, value));}}
in.close();
cout <<"load dict sucess"<< endl;}voidreload(int signo){(void)signo;initDict();}staticvoiddebugPrint(){for(auto&dt : dict){
cout << dt.first <<"#"<< dt.second << endl;}}// demo1voidhandlerMessage(int sockfd, string clientip,uint16_t clientport, string message){// 就可以对message进行特定的业务处理,而不关心message怎么来的 ---- server通信和业务逻辑解耦!
string response_massage;auto iter = dict.find(message);if(iter == dict.end())
response_massage ="unknown";else
response_massage = iter->second;// 开始返回structsockaddr_in client;bzero(&client,sizeof(client));
client.sin_family = AF_INET;
client.sin_port =htons(clientport);
client.sin_addr.s_addr =inet_addr(clientip.c_str());sendto(sockfd, response_massage.c_str(),sizeof(response_massage),0,(structsockaddr*)&client,sizeof(client));}// demo2voidexecCommand(int sockfd, string clientip,uint16_t clientport, string cmd){if(cmd.find("rm")!= string::npos || cmd.find("mv")!= string::npos || cmd.find("rmdir")!= string::npos){
cout << clientip <<":"<< clientport <<"正在执行一个非法操作"<< cmd << endl;return;}
string response_message;// 进程程序替换,执行完之后,将结果保存到cmd中
FILE *fp =popen(cmd.c_str(),"r");if(fp ==nullptr)
response_message = cmd +"exec failed";char line[1024];while(fgets(line,sizeof(line), fp)){
response_message += line;}fclose(fp);// 开始返回structsockaddr_in client;bzero(&client,sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr =inet_addr(clientip.c_str());
client.sin_port =htons(clientport);sendto(sockfd, response_message.c_str(), response_message.size(),0,(structsockaddr*)&client,sizeof(client));}
onlineUser onlineuser;// demo3voidrouteMessage(int sockfd, string clientip,uint16_t clientport, string message){if(message =="online")
onlineuser.addUser(clientip, clientport);if(message =="offline")
onlineuser.delUser(clientip, clientport);if(onlineuser.isOnline(clientip, clientport)){
onlineuser.broadcastMessage(sockfd, clientip, clientport, message);}else{structsockaddr_in client;bzero(&client,sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr =inet_addr(clientip.c_str());
client.sin_port =htons(clientport);
string response ="你还没有上线,请先上线,运行: online";sendto(sockfd, response.c_str(), response.size(),0,(structsockaddr*)&client,sizeof(client));}}// ./udpServer portintmain(int argc,char*argv[]){if(argc !=2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port =atoi(argv[1]);// string ip = argv[1];// signal(2, reload);// initDict();// debugPrint();// std::unique_ptr<udpServer> usvr(new udpServer(handlerMessage, port));// std::unique_ptr<udpServer> usvr(new udpServer(execCommand, port));
std::unique_ptr<udpServer>usvr(newudpServer(routeMessage, port));
usvr->initeServer();
usvr->start();return0;}
3.udpClient.hpp
#pragmaonce#include<iostream>#include<string>#include<cstring>#include<cerrno>#include<cstdlib>#include<strings.h>#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<pthread.h>namespace client
{usingnamespace std;classudpClient{public:udpClient(const string &serverip,constuint16_t serverport):_serverip(serverip),_serverport(serverport),_sockfd(-1),_quit(false){}voidiniteClient(){// 创建socket
_sockfd =socket(AF_INET, SOCK_DGRAM,0);if(_sockfd ==-1){
cerr <<"socket error:"<< errno <<":"<<strerror(errno)<< endl;exit(2);}// 2. client要不要bind[必须要的],client要不要显示的bind,需不需程序员自己bind?不需要!!!// 写服务器的是一家公司,写client是无数家公司 -- 由OS自动形成端口进行bind!-- OS在什么时候,如何bind}// demo1// void run()// {// struct sockaddr_in server;// memset(&server, 0, sizeof(server));// server.sin_family = AF_INET;// server.sin_addr.s_addr = inet_addr(_serverip.c_str());// server.sin_port = htons(_serverport);// string massage;// while (!_quit)// {// cout << "Please Enter#";// cin >> massage;// sendto(_sockfd, massage.c_str(), sizeof(massage), 0, (struct sockaddr *)&server, sizeof(server));// char buffer[1024];// struct sockaddr_in tmp;// socklen_t tmp_len = sizeof(tmp);// ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&tmp, &tmp_len);// if (n > 0)// buffer[n] = 0;// cout << "翻译的结果为:" << buffer << endl;// }// }// void run()// {// struct sockaddr_in server;// memset(&server, 0, sizeof(server));// server.sin_family = AF_INET;// server.sin_addr.s_addr = inet_addr(_serverip.c_str());// server.sin_port = htons(_serverport);// string message;// char cmdline[1024];// while (!_quit)// {// cerr << "[hdp@VM-12-6-centos 2023-10-8]$";// fgets(cmdline, sizeof(cmdline), stdin);// cmdline[strlen(cmdline) - 1] = 0;// message = cmdline;// sendto(_sockfd, message.c_str(), sizeof(message), 0, (struct sockaddr *)&server, sizeof(server));// char buffer[1024];// struct sockaddr_in tmp;// socklen_t tmp_len = sizeof(tmp);// ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&tmp, &tmp_len);// if (n > 0)// buffer[n] = 0;// cout << buffer << endl;// }// }staticvoid*readMessage(void*args){int sockfd =*(static_cast<int*>(args));pthread_detach(pthread_self());while(true){char buffer[1024];structsockaddr_in tmp;
socklen_t tmp_len =sizeof(tmp);
ssize_t n =recvfrom(sockfd, buffer,sizeof(buffer)-1,0,(structsockaddr*)&tmp,&tmp_len);if(n >0)
buffer[n]=0;
cout << buffer << endl;}returnnullptr;}voidrun(){pthread_create(&_reader,nullptr, readMessage,(void*)&_sockfd);structsockaddr_in server;memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr =inet_addr(_serverip.c_str());
server.sin_port =htons(_serverport);
string message;char cmdline[1024];while(!_quit){fprintf(stderr,"Enter# ");fflush(stderr);fgets(cmdline,sizeof(cmdline),stdin);
cmdline[strlen(cmdline)-1]=0;
message = cmdline;sendto(_sockfd, message.c_str(),sizeof(message),0,(structsockaddr*)&server,sizeof(server));}}~udpClient(){}private:int _sockfd;
string _serverip;uint16_t _serverport;bool _quit;
pthread_t _reader;};}
4.udpClient.cc
#include"udpClient.hpp"#include<memory>usingnamespace std;usingnamespace client;voidUsage(const string& proc){
cout<<"\nUsage:\n\t"<<proc<<"local_port\n\n";}// ./udpClient serverip serverportintmain(int argc,char* argv[]){if(argc!=3){Usage(argv[0]);exit(2);}
string serverip = argv[1];uint16_t serverport =atoi(argv[2]);
unique_ptr<udpClient>ucli(newudpClient(serverip,serverport));
ucli->initeClient();
ucli->run();return0;}
5.onlineUser.hpp
#pragmaonce#include<iostream>#include<string>#include<unordered_map>#include<unistd.h>#include<strings.h>#include<sys/types.h>#include<sys/socket.h>#include<arpa/inet.h>#include<netinet/in.h>usingnamespace std;classUser{public:User(const string &ip,constuint16_t&port):_ip(ip),_port(port){}
string ip(){return _ip;}uint16_tport(){return _port;}~User(){}private:
string _ip;uint16_t _port;};classonlineUser{public:onlineUser(){}~onlineUser(){}booladdUser(const string &ip,constuint16_t&port){
string id = ip +"--"+to_string(port);
users.insert(make_pair(id,User(ip, port)));}booldelUser(const string &ip,constuint16_t&port){
string id = ip +"--"+to_string(port);
users.erase(id);}boolisOnline(const string &ip,constuint16_t&port){
string id = ip +"--"+to_string(port);return users.find(id)!= users.end();}voidbroadcastMessage(int sockfd,const string &ip,constuint16_t&port,const string &message){for(auto&user : users){structsockaddr_in client;bzero(&client,sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr =inet_addr(ip.c_str());
client.sin_port =htons(port);
string s = ip +"--"+to_string(port)+"#"+ message;sendto(sockfd, s.c_str(), s.size(),0,(structsockaddr*)&client,sizeof(client));}}private:
unordered_map<string, User> users;};
四、TCP服务器
socket API
1.socket
#include<sys/types.h>#include<sys/socket.h>//创建通信的一端,返回一个文件描述符intsocket(int domain,int type,int protocol);
参数:
domain:
type:
protocol:
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
应用程序可以像读写文件一样用read/write在网络上收发数据;
如果socket()调用出错则返回-1;
对于IPv4, family参数指定为AF_INET;
对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
protocol参数的介绍从略,指定为0即可
2.bind
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
bind()成功返回0,失败返回-1。
bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
我们的程序中对myaddr参数是这样初始化的
structsockaddr_in local;memset(&local,0,sizeof(local));
local.sin_family = AF_INET;
local.sin_port =htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
1.将整个结构体清零;
2.设置地址类型为AF_INET;
3.网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
4.端口号为SERV_PORT, 我们定义为9999
3.listen
listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5)
listen()成功返回0,失败返回-1;
4.accept
三次握手完成后, 服务器调用accept()接受连接;
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
如果给addr 参数传NULL,表示不关心客户端的地址;
addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
我们的服务器程序结构是这样的:
for(;;){structsockaddr_in peer;
socklen_t len =sizeof(peer);int sock =accept(_listensock,(structsockaddr*)&peer,&len);}
5.connect
客户端需要调用connect()连接服务器;
connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
connect()成功返回0,出错返回-1
setsockopt
函数是用于设置套接字选项的函数。它允许我们在创建套接字后对其进行配置和调整,以满足特定的需求该函数的原型如下:
intsetsockopt(int sockfd,int level,int optname,constvoid*optval,socklen_t optlen);// sockfd:表示待设置选项的套接字描述符。// level:表示选项所属的协议层或者协议族,通常使用 SOL_SOCKET 表示操作套接字级别的选项。// optname:表示要设置的选项名称。// optval:是一个指向包含新选项值的缓冲区的指针。// optlen:表示 optval 缓冲区的长度。
SO_REUSEADDR
:允许多个套接字绑定到同一个地址(在服务器程序中常用)。SO_KEEPALIVE
:定期发送心跳信号以检测连接是否仍然活跃。SO_RCVBUF
和SO_SNDBUF
:分别设置接收和发送缓冲区的大小。TCP_NODELAY
:禁用 Nagle 算法,即禁用数据包的延迟发送。IP_TTL
:设置 IP 数据包的生存时间(TTL)。
使用
setsockopt
函数时,需要注意以下几点:
- 在调用
socket
函数创建套接字后,必须在bind
或者connect
函数之前使用setsockopt
设置选项。 level
参数通常为SOL_SOCKET
,用于设置套接字级别的选项。其他可能的值包括IPPROTO_TCP
、IPPROTO_IP
等。optval
缓冲区的类型和大小取决于选项的要求。例如,如果选项需要一个整数值,则optval
应该是一个指向整数的指针,并且optlen
应该是sizeof(int)
。- 函数调用成功时,返回值为0;失败时,返回-1,并设置
errno
变量以指示错误类型
TCP服务器简单实现
以下实现的TCP服务器的功能是对客户端发送过来的数据进行回显
我们这里对网络套接字部分,锁,线程,线程池,守护进程的实现以及日志函数进行了封装实现。
但是在服务端会有如下版本:
1.单进程版本
2.多进程版本
3.多线程版本
4.线程池版本
5.守护进程
1.tcpServer.hpp
#pragmaonce#include<iostream>#include<string>#include<cstring>#include<cstdlib>#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<sys/wait.h>#include<signal.h>#include<pthread.h>#include"log.hpp"#include"Task.hpp"#include"Thread.hpp"#include"ThreadPool.hpp"usingnamespace ThreadNs;staticconstuint16_t gport =8080;classtcpServer{enum{
USAGE_ERR =1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};conststaticint gbacklog =5;classThreadData{public:ThreadData(tcpServer *self,int sock):_self(self),_sock(sock){}public:
tcpServer *_self;int _sock;};public:tcpServer(constuint16_t&port = gport):_port(port),_listensock(-1){}voidinitServer(){// 1. 创建socket文件套接字对象
_listensock =socket(AF_INET, SOCK_STREAM,0);if(_listensock <0){logMessage(FATAL,"create socket error");exit(SOCKET_ERR);}logMessage(NORMAL,"create socket success:%d", _listensock);// 2. bind绑定自己的网络信息structsockaddr_in local;memset(&local,0,sizeof(local));
local.sin_family = AF_INET;
local.sin_port =htons(_port);
local.sin_addr.s_addr = INADDR_ANY;int n =bind(_listensock,(structsockaddr*)&local,sizeof(local));if(n <0){logMessage(FATAL,"bind socket error");exit(BIND_ERR);}logMessage(NORMAL,"bind socket success");// 3. 设置socket 为监听状态
n =listen(_listensock, gbacklog);if(n <0){logMessage(FATAL,"listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL,"listen socket success");}// version 1// void start()// {// for (;;)// {// // 4. server 获取新链接// struct sockaddr_in peer;// socklen_t len = sizeof(peer);// int sock = accept(_listensock, (struct sockaddr *)&peer, &len);// if (sock < 0)// {// logMessage(ERROR, "accetpt error,next");// continue;// }// logMessage(NORMAL, "accept a new link success, get new sock: %d", sock);// // version 1// serviceIO(sock);// close(sock);// }// }// 2 多进程版(2)// void start()// {// signal(SIGCHLD, SIG_IGN);// for (;;)// {// // 4. server 获取新链接// struct sockaddr_in peer;// socklen_t len = sizeof(peer);// int sock = accept(_listensock, (struct sockaddr *)&peer, &len);// if (sock < 0)// {// logMessage(ERROR, "accetpt error,next");// continue;// }// logMessage(NORMAL, "accept a new link success, get new sock: %d", sock);// // version 2 多进程版(2)// pid_t id = fork();// if (id == 0)// {// close(_listensock);// // if (fork() > 0)// // exit(0);// serviceIO(sock);// close(sock);// exit(0);// }// pid_t ret = waitpid(id, nullptr, 0);// if (ret > 0)// {// std::cout << "wait sucess " << ret << std::endl;// }// }// }voidstart(){// ThreadPool<Task>::getInstance()->run();// logMessage(NORMAL, "Thread init success");for(;;){// 4. server 获取新链接structsockaddr_in peer;
socklen_t len =sizeof(peer);int sock =accept(_listensock,(structsockaddr*)&peer,&len);if(sock <0){logMessage(ERROR,"accetpt error,next");continue;}logMessage(NORMAL,"accept a new link success, get new sock: %d", sock);
pthread_t tid;
ThreadData *td =newThreadData(this, sock);pthread_create(&tid,nullptr, threadRoutine, td);pthread_join(tid,nullptr);// version4 线程池// ThreadPool<Task>::getInstance()->push(Task(sock, serviceIO));}}staticvoid*threadRoutine(void*args){pthread_detach(pthread_self());
ThreadData *td =static_cast<ThreadData *>(args);
td->_self->serviceIO(td->_sock);close(td->_sock);delete td;returnnullptr;}voidserviceIO(int sock){char buffer[1024];while(true){
ssize_t n =read(sock, buffer,sizeof(buffer)-1);if(n >0){// 目前我们把读到的数据当成字符串
buffer[n]=0;
std::cout <<"recv message: "<< buffer << std::endl;
std::string outbuffer = buffer;
outbuffer +="[server echo]";write(sock, outbuffer.c_str(), outbuffer.size());}elseif(n ==0){// 代表client退出logMessage(NORMAL,"client quit, me too!");break;}}close(sock);}~tcpServer(){}private:int _listensock;uint16_t _port;};
2.tcpServer.cc
#include"tcpServer.hpp"#include"daemon.hpp"#include<memory>voidUsage(char*proc){
std::cout <<"\nUsage\n\t"<< proc <<"loacl_port\n\n";}// ./tcpserver 8080intmain(int argc,char*argv[]){if(argc !=2){Usage(argv[0]);exit(1);}uint16_t port =atoi(argv[1]);
std::unique_ptr<tcpServer>tsvr(newtcpServer(port));
tsvr->initServer();deamonSelf();
tsvr->start();return0;}
3.tcpClient.hpp
#pragmaonce#include<iostream>#include<string>#include<cstring>#include<cstdlib>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include"log.hpp"classtcpClient{public:tcpClient(const std::string &serverip,constuint16_t&serverport):_sock(-1),_serverip(serverip),_serverport(serverport){}voidinitClient(){// 1. 创建socket
_sock =socket(AF_INET, SOCK_STREAM,0);if(_sock <0){logMessage(FATAL,"create socket error");exit(2);}}voidstart(){structsockaddr_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());int n =connect(_sock,(structsockaddr*)&server,sizeof server);if(n <0){logMessage(ERROR,"connect error");}else{
std::string message;while(true){
std::cout <<"Please Enter#";getline(std::cin, message);write(_sock, message.c_str(), message.size());char buffer[1024];int n =read(_sock, buffer,sizeof(buffer)-1);if(n >0){
buffer[n]=0;
std::cout <<"server 回显# "<< buffer << std::endl;}elsebreak;}}}~tcpClient(){if(_sock >0)close(_sock);}private:int _sock;
std::string _serverip;uint16_t _serverport;};
4.tcpClient.cc
#include"tcpClient.hpp"#include<memory>voidUsage(char*proc){
std::cout <<"\nUsage\n\t"<< proc <<"loacl_port\n\n";}// ./tcpClient serverip serverportintmain(int argc,char* argv[]){if(argc!=3){Usage(argv[0]);exit(1);}
std::string serverip = argv[1];uint16_t serverport =atoi(argv[2]);
std::unique_ptr<tcpClient>tcli(newtcpClient(serverip,serverport));
tcli->initClient();
tcli->start();return0;}
5.Thread.hpp
#pragmaonce#include<iostream>#include<string>#include<functional>#include<cstring>#include<cassert>#include<pthread.h>namespace ThreadNs
{typedef std::function<void*(void*)> func_t;constint num =1024;classThread{private:staticvoid*start_routine(void*args){
Thread *td =static_cast<Thread *>(args);return td->callback();}public:Thread(){char buffer[num];snprintf(buffer,sizeof buffer,"thread-%d", threadnum++);
_name = buffer;}voidstart(func_t func,void*args){
_func = func;
_args = args;int n =pthread_create(&_tid,nullptr, start_routine,this);}voidjoin(){int n =pthread_join(_tid,nullptr);assert(n ==0);(void)n;}
std::string threadname(){return _name;}void*callback(){return_func(_args);}~Thread(){}private:
std::string _name;void*_args;
func_t _func;
pthread_t _tid;staticint threadnum;};int Thread::threadnum =1;}
6.ThreadPool.hpp
#pragmaonce#include"Thread.hpp"#include"LockGuard.hpp"usingnamespace ThreadNs;#include<vector>#include<queue>#include<iostream>constint gnum =3;template<classT>classThreadPool;template<classT>classThreadData{public:ThreadData(ThreadPool<T>*tp,const std::string &threadname):_threadpool(tp),_threadname(threadname){}~ThreadData(){}public:
ThreadPool<T>*_threadpool;
std::string _threadname;};template<classT>classThreadPool{private:staticvoid*handleTask(void* args){
ThreadData<T>* td =static_cast<ThreadData<T>*>(args);while(true){
T t;{
LockGuard lockguard(td->_threadpool->mutex());while(td->_threadpool->isQueueEmpty()){
td->_threadpool->threadWait();}
t = td->_threadpool->pop();}
std::cout << td->_threadname <<" 获取了一个任务: "<< t.toTaskString()<<" 并处理完成,结果是:"<<t()<< std::endl;}delete td;returnnullptr;}public:boolisQueueEmpty(){return _task_queue.empty();}voidthreadWait(){pthread_cond_wait(&_cond,&_mutex);}voidlockQueue(){pthread_mutex_lock(&_mutex);}voidunlockQueue(){pthread_mutex_unlock(&_mutex);}
T pop(){
T t = _task_queue.front();
_task_queue.pop();return t;}
pthread_mutex_t*mutex(){return&_mutex;}public:ThreadPool(constint&num = gnum):_num(num){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_cond,nullptr);for(int i =0; i < _num; i++){
_threads.push_back(newThread());}}public:voidrun(){for(constauto&iter : _threads){
ThreadData<T>*td =newThreadData<T>(this, iter->threadname());
iter->start(handleTask, td);
std::cout << iter->threadname()<<" start..."<< std::endl;}}voidpush(T& in){
LockGuard lockguard(&_mutex);
_task_queue.push(in);pthread_cond_signal(&_cond);}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);for(constauto&t : _threads){delete t;}}private:int _num;
std::vector<Thread *> _threads;
std::queue<T> _task_queue;
pthread_mutex_t _mutex;
pthread_cond_t _cond;};
7.log.hpp
#pragmaonce#defineDEBUG0#defineNORMAL1#defineWARNING2#defineERROR3#defineFATAL4#defineLOG_NORMAL"log.txt"#defineLOG_ERR"log.error"#defineNUM1024#include<iostream>#include<sys/types.h>#include<unistd.h>#include<stdarg.h>constchar*to_levelstr(int level){switch(level){case DEBUG:return"DEBUG";case NORMAL:return"NORMAL";case WARNING:return"WARNING";case ERROR:return"ERROR";case FATAL:return"FATAL";default:returnnullptr;}}voidlogMessage(int level,constchar*format,...){// [日志等级] [时间戳/时间] [pid] [messge]char logprofix[NUM];snprintf(logprofix,sizeof logprofix,"[%s][%ld][pid:%d]",to_levelstr(level),(longint)time(nullptr),getpid());char logcontent[NUM];
va_list arg;va_start(arg, format);vsnprintf(logcontent,sizeof logcontent, format, arg);
std::cout << logprofix << logcontent << std::endl;
FILE *log =fopen(LOG_NORMAL,"a");
FILE *error =fopen(LOG_ERR,"a");if(log && error){
FILE *cur =nullptr;if(level == DEBUG || level == NORMAL || level == WARNING)
cur = log;if(level == ERROR || level == FATAL)
cur = error;if(cur)fprintf(cur,"%s%s\n", logprofix, logcontent);fclose(log);fclose(error);}}
8.LockGuard.hpp
#pragmaonce#include<cassert>#include<pthread.h>classMutex{public:Mutex(pthread_mutex_t *lock_p =nullptr):_lock_p(lock_p){}voidlock(){if(_lock_p){int n =pthread_mutex_lock(_lock_p);assert(n ==0);(void)n;}}voidunlock(){if(_lock_p){int n =pthread_mutex_unlock(_lock_p);assert(n ==0);(void)n;}}~Mutex(){}private:
pthread_mutex_t *_lock_p;};classLockGuard{public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){
_mutex.lock();}~LockGuard(){
_mutex.unlock();}private:
Mutex _mutex;};
9.daemon.hpp
#pragmaonce#include<unistd.h>#include<signal.h>#include<cstdlib>#include<cassert>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#defineDEV"/dev/null"voiddeamonSelf(constchar*curpath =nullptr){// 1. 让调用进程忽略掉异常的信号signal(SIGPIPE, SIG_IGN);// 2. 如何让自己不是组长,setsidif(fork()>0)exit(0);// 子进程 -- 守护进程,精灵进程,本质就是孤儿进程的一种!
pid_t n =setsid();assert(n !=-1);// 3. 守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件int fd =open(DEV, O_WRONLY);if(fd >=0){dup2(fd,0);dup2(fd,1);dup2(fd,2);}else{close(0);close(1);close(2);}// 4. 可选:进程执行路径发生更改if(curpath)chdir(curpath);}
10.Task.hpp
#pragmaonce#include<iostream>#include<string>#include<functional>#include<unistd.h>#include"log.hpp"voidserviceIO(int sock){char buffer[1024];while(true){
ssize_t n =read(sock, buffer,sizeof(buffer)-1);if(n >0){// 目前我们把读到的数据当成字符串
buffer[n]=0;
std::cout <<"recv message: "<< buffer << std::endl;
std::string outbuffer = buffer;
outbuffer +="[server echo]";write(sock, outbuffer.c_str(), outbuffer.size());}elseif(n ==0){// 代表client退出logMessage(NORMAL,"client quit, me too!");break;}}close(sock);}classTask{typedef std::function<void(int)> func_t;public:Task(){}Task(constint sock, func_t &func):_sock(sock),_callback(func){}voidoperator()(){_callback(_sock);}private:int _sock;
func_t _callback;};
注意事项:
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配
客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动多个客户端, 就会出现端口号被占用导致不能正确建立连接;
服务器也不是必须调用bind(), 但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦
五、TCP协议通讯流程
下图是基于TCP协议的客户端/服务器程序的一般流程:
服务器初始化:
调用socket, 创建文件描述符;
调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
调用accecpt, 并阻塞, 等待客户端连接过来
建立连接的过程:
调用socket, 创建文件描述符;
调用connect, 向服务器发起连接请求;
connect会发出SYN段并阻塞等待服务器应答; (第一次)
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手;
数据传输的过程
建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
版权归原作者 椿融雪 所有, 如有侵权,请联系我们删除。