在哪里做網(wǎng)站比較好十大廣告投放平臺
?小白苦學(xué)IT的博客主頁
?初學(xué)者必看:Linux操作系統(tǒng)入門
?代碼倉庫:Linux代碼倉庫
?關(guān)注我一起討論和學(xué)習(xí)Linux系統(tǒng)
1.前言
網(wǎng)絡(luò)編程前言
網(wǎng)絡(luò)編程是連接數(shù)字世界的橋梁,它讓計(jì)算機(jī)之間能夠交流信息,為我們的生活和工作帶來便利。從簡單的網(wǎng)頁瀏覽到復(fù)雜的分布式系統(tǒng),網(wǎng)絡(luò)編程無處不在。
然而,網(wǎng)絡(luò)編程涉及諸多復(fù)雜概念和技術(shù),如IP地址、端口號、Socket、TCP/UDP協(xié)議等,需要我們深入學(xué)習(xí)和掌握。同時(shí),網(wǎng)絡(luò)環(huán)境的復(fù)雜性、數(shù)據(jù)安全性等問題也帶來了挑戰(zhàn)。
但正是這些挑戰(zhàn),讓網(wǎng)絡(luò)編程充滿了無限可能。掌握網(wǎng)絡(luò)編程技術(shù),我們可以開發(fā)出各種創(chuàng)新應(yīng)用,為人們提供更高效、智能的服務(wù)。
本文旨在介紹網(wǎng)絡(luò)編程的Socket編程接口及其技術(shù),分享實(shí)用經(jīng)驗(yàn),幫助讀者打下堅(jiān)實(shí)的網(wǎng)絡(luò)編程基礎(chǔ)。
1.socket編程接口
socket常見API
// 創(chuàng)建 socket 文件描述符 (TCP/UDP, 客戶端 + 服務(wù)器)
int socket(int domain, int type, int protocol);
// 綁定端口號 (TCP/UDP, 服務(wù)器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 開始監(jiān)聽socket (TCP, 服務(wù)器)
int listen(int socket, int backlog);
// 接收請求 (TCP, 服務(wù)器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立連接 (TCP, 客戶端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockaddr結(jié)構(gòu)
socket API是一層抽象的網(wǎng)絡(luò)編程接口,適用于各種底層網(wǎng)絡(luò)協(xié)議,如IPv4、IPv6,以及后面要談的UNIX DomainSocket. 然而, 各種網(wǎng)絡(luò)協(xié)議的地址格式并不相同.
- IPv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結(jié)構(gòu)體表示,包括16位地址類型, 16位端口號和32位IP地址。
- IPv4、IPv6地址類型分別定義為常數(shù)AF_INET、AF_INET6. 這樣,只要取得某種sockaddr結(jié)構(gòu)體的首地址,不需要知道具體是哪種類型的sockaddr結(jié)構(gòu)體,就可以根據(jù)地址類型字段確定結(jié)構(gòu)體中的內(nèi)容。
- socket API可以都用struct sockaddr *類型表示, 在使用的時(shí)候需要強(qiáng)制轉(zhuǎn)化成sockaddr_in; 這樣的好處是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各種類型的sockaddr結(jié)構(gòu)體指針做為參數(shù)。
sockaddr 結(jié)構(gòu)?
sockaddr
?是一個通用的套接字地址結(jié)構(gòu),它用于表示各種類型的套接字地址。但是,sockaddr
?結(jié)構(gòu)本身并不包含足夠的信息來確定地址的類型,因此它通常被更具體的結(jié)構(gòu)(如?sockaddr_in
)所替代。sockaddr
?結(jié)構(gòu)的主要作用是為不同的地址結(jié)構(gòu)提供一個統(tǒng)一的接口。
- 通用性:
sockaddr
是一個通用的套接字地址結(jié)構(gòu),設(shè)計(jì)初衷是為了能夠表示各種類型的套接字地址,包括IPv4、IPv6以及其他可能的地址類型。這種通用性使得sockaddr
能夠作為許多網(wǎng)絡(luò)編程函數(shù)的參數(shù),如bind
、connect
、recvfrom
、sendto
等,用于指明地址信息。- 擴(kuò)展性:通過定義
sa_family
字段,sockaddr
能夠區(qū)分不同類型的地址結(jié)構(gòu)。這使得在未來引入新的地址類型時(shí),不需要修改現(xiàn)有函數(shù)的接口,只需定義新的地址結(jié)構(gòu)并設(shè)置相應(yīng)的sa_family
即可。
sockaddr_in 結(jié)構(gòu)
sockaddr_in
?是?sockaddr
?結(jié)構(gòu)的一個特例,用于表示 IPv4 地址和端口號。它包含了 IP 地址和端口號的信息,以及地址族和協(xié)議信息。
- IPv4特化:盡管
sockaddr
具有通用性,但在實(shí)際編程中,特別是在處理IPv4地址時(shí),直接使用sockaddr
結(jié)構(gòu)會顯得過于復(fù)雜和冗余。sockaddr_in
結(jié)構(gòu)是針對IPv4地址設(shè)計(jì)的,它包含了IPv4地址和端口號等必要信息,并且以更直觀和易于操作的方式呈現(xiàn)這些信息。- 便利性:
sockaddr_in
提供了專門的字段來存儲IPv4地址(sin_addr
)和端口號(sin_port
),這使得在處理IPv4網(wǎng)絡(luò)編程任務(wù)時(shí)更加方便和高效。同時(shí),通過類型轉(zhuǎn)換,sockaddr_in
結(jié)構(gòu)可以很容易地轉(zhuǎn)換為sockaddr
結(jié)構(gòu),從而與需要sockaddr
參數(shù)的函數(shù)兼容。
in_addr結(jié)構(gòu)?
in_addr
?結(jié)構(gòu)用于表示一個 IPv4 地址。它通常與?sockaddr_in
?結(jié)構(gòu)一起使用,作為?sin_addr
?字段的類型。
在這個結(jié)構(gòu)中,s_addr
?是一個無符號長整數(shù),表示 IPv4 地址。在實(shí)際使用中,我們通常不會直接操作這個長整數(shù),而是使用諸如?inet_pton
?和?inet_ntop
?這樣的函數(shù)來將點(diǎn)分十進(jìn)制格式的 IP 地址(如 "192.168.1.1")轉(zhuǎn)換為?in_addr
?結(jié)構(gòu),或者將?in_addr
?結(jié)構(gòu)轉(zhuǎn)換為點(diǎn)分十進(jìn)制格式的字符串。?
- IPv4地址表示:
in_addr
結(jié)構(gòu)專門用于表示IPv4地址。它通過一個無符號長整數(shù)(s_addr
)來存儲IPv4地址,這種表示方式在網(wǎng)絡(luò)編程中非常常見。盡管IPv4地址通常以點(diǎn)分十進(jìn)制的形式表示(如192.168.1.1),但在內(nèi)部處理和網(wǎng)絡(luò)傳輸時(shí),它們通常被轉(zhuǎn)換為這種整數(shù)形式。- 轉(zhuǎn)換方便:
in_addr
結(jié)構(gòu)使得在點(diǎn)分十進(jìn)制格式和內(nèi)部整數(shù)格式之間轉(zhuǎn)換IPv4地址變得相對簡單。通過調(diào)用如inet_pton
和inet_ntop
這樣的函數(shù),可以輕松實(shí)現(xiàn)這兩種格式之間的轉(zhuǎn)換,從而方便網(wǎng)絡(luò)編程中的地址處理。
總結(jié)一下就是:
sockaddr
?是一個通用的套接字地址結(jié)構(gòu),用于表示各種類型的地址。sockaddr_in
?是?sockaddr
?的一個特例,用于表示 IPv4 地址和端口號。in_addr
?用于表示 IPv4 地址。
這三種結(jié)構(gòu)的存在是為了滿足不同網(wǎng)絡(luò)編程需求和提高編程效率。sockaddr
提供了通用性和擴(kuò)展性,sockaddr_in
則針對IPv4地址提供了更直觀和便利的操作方式,而in_addr
則專門用于表示和轉(zhuǎn)換IPv4地址。在實(shí)際編程中,根據(jù)具體需求選擇合適的結(jié)構(gòu)進(jìn)行處理,可以提高代碼的可讀性和可維護(hù)性。
2.簡單UDP的echo服務(wù)器(代碼實(shí)現(xiàn))
封裝 UdpSocket
UdpServer.hpp
默認(rèn)ip用 0.0.0.0
端口:8080
對udp服務(wù)器進(jìn)行封裝:
#pragma once
#include "Log.hpp"
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<errno.h>
#include<functional>using func_t = std::function<std::string (const std::string&,uint16_t &,const std::string&)>;uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;enum
{SOCKET_ERR = 1,BIND_ERR};class UdpServer
{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):_sockfd(-1), _port(port), _ip(ip),_isrunning(false){}void Init(){//1.創(chuàng)建udp socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){log.LogMessage(FATAL, "socket create error,_sockfd: %d", _sockfd);exit(SOCKET_ERR);}log.LogMessage(INFO, "socket create success, _sockfd: %d ", _sockfd);//2.bind socketstruct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);//需要保證我的端口號是網(wǎng)絡(luò)字節(jié)序列,因?yàn)樵摱丝谔柺且o對方發(fā)送的。local.sin_addr.s_addr = inet_addr(_ip.c_str()); //1.string->uint32_t 2.uint32_t 必須是網(wǎng)絡(luò)序列的//local.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(_sockfd,(const struct sockaddr *)&local,sizeof(local))<0){log.LogMessage(FATAL,"bind error , error: %d, error string : %s",errno,strerror(errno));exit(BIND_ERR);}log.LogMessage(INFO,"bind success , error: %d, error string : %s",errno,strerror(errno));}void Run(func_t func){_isrunning = true;char inbuffer[size];while(_isrunning){struct sockaddr_in client;socklen_t len = sizeof(client);std::cout<<"server is run!!!"<<std::endl;ssize_t n = recvfrom(_sockfd,inbuffer,sizeof(inbuffer) - 1,0,(struct sockaddr *)&client,&len);if(n<0){log.LogMessage(WARNING,"recvfrom error, errno: %d ,errno string : %s",errno,strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);inbuffer[n] = 0;//充當(dāng)了一次數(shù)據(jù)的處理std::string info = inbuffer;std::string echo_string = func(info,clientport,clientip);sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&client,len);}}~UdpServer() {if(_sockfd>0) close(_sockfd);}private:int _sockfd;//網(wǎng)絡(luò)文件描述符std::string _ip;//字符串類型ip地址uint16_t _port;//服務(wù)器進(jìn)程的端口號 bool _isrunning;
};
Main.cc
#include"UdpServer.hpp"
#include<memory>
#include<iostream>
#include<cstdio>
#include<vector>void Usage(std::string proc)
{std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}std::string Handler(const std::string& str,uint16_t & clientport,const std::string& clientip)
{std::cout<<"[ ip: "<< clientip<<" port: "<<clientport<<" ]# ";std::string res = "server get a message: ";res+=str;std::cout<<res<<std::endl;return res;
}bool SafeCheck(const std::string & cmd)
{std::vector<std::string> key_word = {"rm","mv","cp","kill","sudo","unlink","uninstall","yum","top"};for(auto &word:key_word){auto pos = cmd.find(word);if(pos!=std::string::npos) return false;}return true;
}std::string ExcuteCommand(const std::string & cmd)
{if(!SafeCheck(cmd)) return "bad man";FILE* fp = popen(cmd.c_str(),"r");if(nullptr == fp){perror("popen error");return "error";}std::string result;char buffer[4096];while(true){char * getc = fgets(buffer,sizeof(buffer),fp);if(nullptr == getc){break;}result+=buffer;}pclose(fp);return result;}int main(int argc,char* argv[])
{if(argc!=2){Usage(argv[0]);exit(1);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init();svr->Run(Handler); return 0;
}
UdpClient.cc(客戶端代碼)
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}// ./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]);struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport); //?server.sin_addr.s_addr = inet_addr(serverip.c_str());socklen_t len = sizeof(server);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socker error" << endl;return 1;}// client 要bind嗎?要!只不過不需要用戶顯示的bind!一般有OS自由隨機(jī)選擇!// 一個端口號只能被一個進(jìn)程bind,對server是如此,對于client,也是如此!// 其實(shí)client的port是多少,其實(shí)不重要,只要能保證主機(jī)上的唯一性就可以!// 系統(tǒng)什么時(shí)候給我bind呢?首次發(fā)送數(shù)據(jù)的時(shí)候string message;char buffer[1024];while (true){cout << "Please Enter@ ";getline(cin, message);cout<<message<<endl;// std::cout << message << std::endl;// 1. 數(shù)據(jù) 2. 給誰發(fā)sendto(sockfd, message.c_str(), message.size(), 0, (const sockaddr *)&server, len);struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0) {buffer[s] = 0;cout << buffer << endl;}}close(sockfd);return 0;
}
日志類:
#pragma once#include <iostream>
#include <cstdarg>
#include <ctime>
#include <string>
#include <unistd.h>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>enum
{DEBUG = 0,INFO,WARNING,ERROR,FATAL
};enum
{Screen = 10,Onefile,Classfile
};std::string LevelToString(int level)
{switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknown";}
}const int defaultstyle = Screen;
const std::string default_filename = "log.";
const std::string logdir="log";class Log
{
public:Log():style(defaultstyle),filename(default_filename){mkdir(logdir.c_str(),0775);}void Enable(int sty){style = sty;}std::string TimestampToLocalTime(){time_t curr = time(nullptr);struct tm *currtime = localtime(&curr);char time_buffer[128];snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",currtime->tm_year + 1900, currtime->tm_mon, currtime->tm_mday, currtime->tm_hour,currtime->tm_min, currtime->tm_sec);return time_buffer;}void WriteLog(const std::string &levelstr, const std::string &message){switch (style){case Screen:std::cout << message<<std::endl;break;case Onefile:WriteLogToOnefile("all", message);break;case Classfile:WriteLogToClassfile(levelstr, message);break;default:break;}}void WriteLogToOnefile(const std::string &logname, const std::string &message){umask(0);int fd = open(logname.c_str(),O_CREAT | O_WRONLY | O_APPEND,0666);if(fd<0)return;write(fd,message.c_str(),message.size());close(fd);// std::ofstream out(logname);// if (!out.is_open())// return;// out.write(message.c_str(), message.size());// out.close();}void WriteLogToClassfile(const std::string &levelstr, const std::string &message){std::string logname = logdir;logname+="/";logname+=filename;logname += levelstr;WriteLogToOnefile(logname, message);}void LogMessage(int level, const char *format, ...) // 類c的日志接口{char rightbuffer[1024];va_list args;va_start(args, format);vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);va_end(args);char leftbuffer[1024];std::string curtime = TimestampToLocalTime();std::string levelstr = LevelToString(level);std::string idstr = std::to_string(getpid());snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s]",levelstr.c_str(), curtime.c_str(), idstr.c_str());std::string logInfo = leftbuffer;logInfo += rightbuffer;WriteLog(levelstr, logInfo);}~Log() {}private:int style;std::string filename;
};Log log;class Conf
{
public:Conf(){log.Enable(Screen);}~Conf(){}
};Conf conf;
Makefile
.PHONY:all
all:udpserver udpclientudpserver:Main.ccg++ -o $@ $^ -g -std=c++11
udpclient:UdpClient.ccg++ -o $@ $^ -g -std=c++11.PHONY:clean
clean:rm -rf udpserver udpclient
運(yùn)行結(jié)果:
實(shí)現(xiàn)了客戶端,服務(wù)端雙方交互,當(dāng)然我們這只是簡單的進(jìn)行數(shù)據(jù)處理,其實(shí)還可以通過實(shí)現(xiàn)其他功能,這里可以發(fā)揮自己的想象去寫。
地址轉(zhuǎn)換函數(shù)
這里只介紹基于IPv4的socket網(wǎng)絡(luò)編程,sockaddr_in中的成員struct in_addr sin_addr表示32位 的IP 地址,但是我們通常用點(diǎn)分十進(jìn)制的字符串表示IP 地址,以下函數(shù)可以在字符串表示 和in_addr表示之間轉(zhuǎn)換;
字符串轉(zhuǎn)in_addr的函數(shù):
in_addr轉(zhuǎn)字符串的函數(shù):
其中inet_pton和inet_ntop不僅可以轉(zhuǎn)換IPv4的in_addr,還可以轉(zhuǎn)換IPv6的in6_addr,因此函數(shù)接口是void*addrptr。
關(guān)于inet_ntoa
inet_ntoa這個函數(shù)返回了一個char*, 很顯然是這個函數(shù)自己在內(nèi)部為我們申請了一塊內(nèi)存來保存ip的結(jié)果. 那么是否需要調(diào)用者手動釋放呢?
man手冊上說, inet_ntoa函數(shù), 是把這個返回結(jié)果放到了靜態(tài)存儲區(qū). 這個時(shí)候不需要我們手動進(jìn)行釋放.
那么問題來了, 如果我們調(diào)用多次這個函數(shù), 會有什么樣的效果呢? 參見如下代碼:
#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;
}
運(yùn)行結(jié)果:
因?yàn)閕net_ntoa把結(jié)果放到自己內(nèi)部的一個靜態(tài)存儲區(qū), 這樣第二次調(diào)用時(shí)的結(jié)果會覆蓋掉上一次的結(jié)果
- 如果有多個線程調(diào)用 inet_ntoa, 是否會出現(xiàn)異常情況呢?
- 在APUE中, 明確提出inet_ntoa不是線程安全的函數(shù);
- 但是在centos7上測試, 并沒有出現(xiàn)問題, 可能內(nèi)部的實(shí)現(xiàn)加了互斥鎖;
- 在多線程環(huán)境下, 推薦使用inet_ntop, 這個函數(shù)由調(diào)用者提供一個緩沖區(qū)保存結(jié)果, 可以規(guī)避線程安全問題;
如果測試如下代碼:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);printf("addr1: %s\n", ptr);}return NULL;
}
void *Func2(void *p)
{struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);printf("addr2: %s\n", ptr);}return NULL;
}
int main()
{pthread_t tid1 = 0;struct sockaddr_in addr1;struct sockaddr_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);return 0;
}
運(yùn)行結(jié)果:
這段代碼試圖創(chuàng)建兩個線程,
Func1
?和?Func2
,它們分別無限循環(huán)地打印兩個?sockaddr_in
?結(jié)構(gòu)的 IP 地址。這兩個?sockaddr_in
?結(jié)構(gòu),addr1
?和?addr2
,被初始化為具有特定的?sin_addr.s_addr
?值。
addr1.sin_addr.s_addr
?被初始化為?0
,這在 IPv4 地址中通常表示一個未指定的地址,或者說是無效的地址。
addr2.sin_addr.s_addr
?被初始化為?0xffffffff
,這在 IPv4 地址中通常表示廣播地址。然而,代碼中有一些需要注意的地方:
- inet_ntoa的靜態(tài)緩沖區(qū):
inet_ntoa
?函數(shù)使用靜態(tài)緩沖區(qū)來存儲轉(zhuǎn)換后的字符串。這意味著如果兩個線程同時(shí)調(diào)用?inet_ntoa
,它們可能會覆蓋彼此的緩沖區(qū),導(dǎo)致不可預(yù)測的結(jié)果。因此,在多線程環(huán)境中使用?inet_ntoa
?是不安全的。- 無限循環(huán):兩個線程都包含一個無限循環(huán),這會導(dǎo)致程序永遠(yuǎn)不會退出,除非被外部因素(如用戶終止)中斷。
- pthread_join:雖然代碼中包含了?
pthread_join
?調(diào)用,但由于線程中的無限循環(huán),這些調(diào)用實(shí)際上永遠(yuǎn)不會返回,因此?main
?函數(shù)也永遠(yuǎn)不會結(jié)束。測試這段代碼時(shí),你會看到兩個線程分別不停地打印出相同的 IP 地址字符串,但由于?
inet_ntoa
?的問題,這些字符串可能會被互相覆蓋,導(dǎo)致輸出變得混亂。此外,具體的輸出取決于操作系統(tǒng)的具體實(shí)現(xiàn)和線程調(diào)度的行為。在某些情況下,你可能會看到?
addr1
?和?addr2
?交替出現(xiàn),而在其他情況下,你可能會看到某個地址連續(xù)出現(xiàn)多次,然后被另一個地址覆蓋。總的來說,這段代碼并不是一個好的示例,因?yàn)樗诙嗑€程環(huán)境中不正確地使用了?
inet_ntoa
,并且包含了無限循環(huán),這會導(dǎo)致程序行為不可預(yù)測且難以管理。如果你需要在多線程環(huán)境中處理 IP 地址,建議使用更安全的函數(shù),如?
inet_ntop
,并確保正確管理線程的生命周期和同步。