文章目錄
- 前言
- 一. 服務器
- 1. 初始化服務器
- 2. 啟動服務器
- 二. 客戶端
- 三. 多進程服務器
- 結束語
前言
本系列文章是計算機網絡學習
的筆記,歡迎大佬們閱讀,糾錯,分享相關知識。希望可以與你共同進步。
本篇博客基于UDP socket基礎,介紹TCP socket編程接口和細節
UDP socket編程可參看【計算機網絡學習之路】UDP socket編程
本次編寫的服務器和客戶端依然是最簡單的echo服務器
一. 服務器
服務器的基本框架:
tcp_server.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>namespace ns_server
{const uint16_t default_port = 8888;class TcpServer{public:TcpServer(uint16_t port = default_port) : _port(port){}void InitServer(){}//初始化服務器void Start(){}//啟動服務器~TcpServer(){}private:int _sock; // 監聽套接字uint16_t _port; // 端口號};
}
tcp_server.cc
#include"tcp_server.hpp"
#include<memory>using namespace std;
using namespace ns_server;static void usage(char*argv)
{cout<<"Usage\n\t"<<argv<<" serverPort"<<endl;
}
int main(int argc,char*argv[])
{if(argc!=2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);unique_ptr<TcpServer> usvr(new TcpServer(echo,port));usvr->InitServer();usvr->Start();return 0;
}
1. 初始化服務器
服務器的初始化,還是一樣的
- 創建套接字
- 綁定套接字
void InitServer()
{// 1.創建套接字_sock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){std::cerr << "create sock error," << strerror(errno) << std::endl;exit(1);}std::cout << "create listensock success: " << _sock << std::endl;// 2.綁定套接字struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_sock , (struct sockaddr *)&local, sizeof(local)) < 0){std::cerr << "bind error," << strerror(errno) << std::endl;exit(2);}
}
需要注意的是,socket的第二個參數為SOCK_STREAM
面向字節流
TCP與UDP不同的地方是,TCP是面向連接的,UDP是無連接的
所以TCP還需要listen
返回值:成功返回0,失敗返回-1并設置錯誤碼
backlog參數需要在后續TCP詳解中學習,先定義大小為32
const int backlog = 32;
void InitServer()
{// 1.創建套接字_sock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){std::cerr << "create sock error," << strerror(errno) << std::endl;exit(1);}std::cout << "create listensock success: " << _sock << std::endl;// 2.綁定套接字struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_sock , (struct sockaddr *)&local, sizeof(local)) < 0){std::cerr << "bind error," << strerror(errno) << std::endl;exit(2);}// 3.監聽if (listen(_listensock, backlog) < 0){std::cerr << "listen error," << strerror(errno) << std::endl;exit(3);}
}
初始化到此就結束了
接下來是啟動服務器
2. 啟動服務器
TCP通過accept獲取客戶端連接
sockfd
:socket返回的文件描述符addr
:輸入輸出型參數,客戶端信息的結構體addrlen
:輸入輸出型參數,結構體大小。注意:需要傳入addr的大小
返回值是網絡文件描述符
在TCP中,socket返回的網絡文件可以理解為連接文件,內部保存了連接信息
而accept是從連接文件中獲取連接,然后創建套接字,網絡文件。
真正通信的是connect創建的網絡文件
我們將私有成員的_sock改為_listensock
void Start()
{while (true){struct sockaddr_in client;memset(&client, 0, sizeof(client));socklen_t len = sizeof(client);int sock = accept(_listensock, (struct sockaddr *)&client, &len);if (sock < 0){std::cerr << "accept error" << std::endl;continue;}// 提取客戶端信息std::string clientIp = inet_ntoa(client.sin_addr);uint16_t clientPort = ntohs(client.sin_port);std::string name = "[" + clientIp + ":" + std::to_string(clientPort) + "]";std::cout << "create sock " << sock << " from " << _listensock << std::endl;}
}
接下來就可以在connect返回的套接字中讀寫數據了。
本次使用read
和write
void Start()
{while (true){struct sockaddr_in client;memset(&client, 0, sizeof(client));socklen_t len = sizeof(client);int sock = accept(_listensock, (struct sockaddr *)&client, &len);if (sock < 0){std::cerr << "accept error" << std::endl;continue;}// 提取客戶端信息std::string clientIp = inet_ntoa(client.sin_addr);uint16_t clientPort = ntohs(client.sin_port);std::string name = "[" + clientIp + ":" + std::to_string(clientPort) + "]";std::cout << "create sock " << sock << " from " << _listensock << std::endl;char buffer[1024];while (true){int n = read(sock, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = '\0';std::cout << name << "# " << buffer << std::endl;std::string responce = buffer;//返回收到的數據int m = write(sock, responce.c_str(), responce.size());}else if (n == 0){// 寫端關閉std::cout << name << " quit,me to" << std::endl;close(sock);break;}else{// 讀數據異常std::cerr << "read error" << std::endl;break;}}}
}
完整代碼:
tcp_server.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>namespace ns_server
{const uint16_t default_port = 8888;const int backlog = 32;class TcpServer{public:TcpServer(func_t func, uint16_t port = default_port) : _port(port), _func(func){}void InitServer(){// 1.創建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){std::cerr << "create sock error," << strerror(errno) << std::endl;exit(1);}std::cout << "create listensock success: " << _listensock << std::endl;// 2.綁定套接字struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){std::cerr << "bind error," << strerror(errno) << std::endl;exit(2);}// 3.監聽if (listen(_listensock, backlog) < 0){std::cerr << "listen error," << strerror(errno) << std::endl;exit(3);}}void Start(){while (true){struct sockaddr_in client;memset(&client, 0, sizeof(client));socklen_t len = sizeof(client);int sock = accept(_listensock, (struct sockaddr *)&client, &len);if (sock < 0){std::cerr << "accept error" << std::endl;continue;}// 提取客戶端信息std::string clientIp = inet_ntoa(client.sin_addr);uint16_t clientPort = ntohs(client.sin_port);std::string name = "[" + clientIp + ":" + std::to_string(clientPort) + "]";std::cout << "create sock " << sock << " from " << _listensock << std::endl;char buffer[1024];while (true){int n = read(sock, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = '\0';std::cout << name << "# " << buffer << std::endl;std::string responce = buffer;int m = write(sock, responce.c_str(), responce.size());}else if (n == 0){// 寫端關閉std::cout << name << " quit,me to" << std::endl;close(sock);break;}else{// 讀數據異常std::cerr << "read error" << std::endl;break;}}}}~TcpServer(){}private:int _listensock; // 監聽套接字uint16_t _port; // 端口號};
}
PS:上述的服務器是單進程,所以只能同時處理一個客戶端,讀者可以嘗試添加一下多進程,多線程或者線程池
本篇博客最后會貼出多進程的方案
二. 客戶端
客戶端就不作封裝了
最開始也是要創建套接字
然后TCP的客戶端需要connect
服務器
sockfd
:socket返回的文件描述符addr
:服務器信息的結構體addrlen
:結構體大小。返回值
:成功返回0,失敗返回-1并設置錯誤碼
注意:connect時OS會bind客戶端
UDP是在發送數據時才會bind
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;static void usage(char *argv)
{cout << "Usage:\n\t" << argv << " serverIp serverPort" << endl;
}int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(USAGE_ERR);}string serverIp = argv[1];uint16_t serverPort = atoi(argv[2]);// 1.創建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){cerr << "create sock error," << strerror(errno) << endl;exit(SOCKET_ERR);}cout<<"create sock sucess:"<<sock<<endl;// 2. 連接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);int cnt = 5; // 記錄重連次數// connect時會bindwhile (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0){cout << "正在重連...還有" << cnt-- << "次" << endl;if (cnt <= 0){ cerr<<"連接失敗"<<endl;exit(CONNECT_ERR);}sleep(1);}// 連接成功string name = "["+serverIp + ":" + to_string(serverPort)+"]";cout << "connect " << name << " sucess" << endl;return 0;
}
然后也可以開始讀寫數據了
完整代碼:
tcp_client.cc
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>using namespace std;static void usage(char *argv)
{cout << "Usage:\n\t" << argv << " serverIp serverPort" << endl;
}int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(USAGE_ERR);}string serverIp = argv[1];uint16_t serverPort = atoi(argv[2]);// 1.創建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){cerr << "create sock error," << strerror(errno) << endl;exit(SOCKET_ERR);}cout<<"create sock sucess:"<<sock<<endl;// 2. 連接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);int cnt = 5; // 記錄重連次數// connect時會bindwhile (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0){cout << "正在重連...還有" << cnt-- << "次" << endl;if (cnt <= 0){ cerr<<"連接失敗"<<endl;exit(CONNECT_ERR);}sleep(1);}// 連接成功string name = "["+serverIp + ":" + to_string(serverPort)+"]";cout << "connect " << name << " sucess" << endl;// 發送消息while (true){cout << "please enter your message# ";string message;getline(cin, message);int n = write(sock, message.c_str(), message.size());if (n < 0){cerr << "write error," << strerror(errno) << endl;break;}else if (n == 0){cout << "讀端關閉,停止寫" << endl;break;}char buffer[1024];int m = read(sock, buffer, sizeof(buffer) - 1);if (m > 0){buffer[n] = '\0';cout<<name<<" echo "<<buffer<<endl;}else if (m == 0){// 寫端關閉std::cout << name << " quit,me to" << std::endl;close(sock);break;}else{// 讀數據異常std::cerr << "read error" << std::endl;break;}}return 0;
}
三. 多進程服務器
tcp_server.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>namespace ns_server
{const uint16_t default_port = 8888;const int backlog = 32;class TcpServer{public:TcpServer(uint16_t port = default_port) : _port(port){}void InitServer(){// 1.創建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){std::cerr << "create sock error," << strerror(errno) << std::endl;exit(1);}std::cout << "create listensock success: " << _listensock << std::endl;// 2.綁定套接字struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){std::cerr << "bind error," << strerror(errno) << std::endl;exit(2);}// 3.監聽if (listen(_listensock, backlog) < 0){std::cerr << "listen error," << strerror(errno) << std::endl;exit(3);}}void Start(){//忽略子進程的信號,不需要等待子進程退出(推薦!!!)signal(SIGCHLD,SIG_IGN);while (true){struct sockaddr_in client;memset(&client, 0, sizeof(client));socklen_t len = sizeof(client);int sock = accept(_listensock, (struct sockaddr *)&client, &len);if (sock < 0){std::cerr << "accept error" << std::endl;continue;}std::cout << "create sock " << sock << " from " << _listensock << std::endl;// 多進程pid_t id = fork();if (id < 0){close(sock);continue;}else if (id == 0){//子進程close(_listensock);//建議關掉不需要的fdif(fork()>0)exit(0);//子進程退掉,后續為孫子進程// 提取客戶端信息std::string clientIp = inet_ntoa(client.sin_addr);uint16_t clientPort = ntohs(client.sin_port);service(sock, clientIp, clientPort);exit(0);}//父進程//一定關掉不需要的fd,防止fd泄露close(sock);//pid_t ret=waitpid(id,nullptr,0);//默認為阻塞等待//pid_t ret=waitpid(id,nullptr,WNOHANG);//非阻塞//if(ret==id) std::cout<<"wait "<<id<<" sucess"<<std::endl;}}void service(int sock, std::string &clientIp, uint16_t&clientPort){std::string name = "[" + clientIp + ":" + std::to_string(clientPort) + "]";char buffer[1024];while (true){int n = read(sock, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = '\0';std::cout << name << "# " << buffer << std::endl;std::string responce = buffer;int m = write(sock, responce.c_str(), responce.size());}else if (n == 0){// 寫端關閉std::cout << name << " quit,me to" << std::endl;close(sock);break;}else{// 讀數據異常std::cerr << "read error" << std::endl;break;}}}~TcpServer(){}private:int _listensock; // 監聽套接字uint16_t _port; // 端口號};
}
結束語
本篇博客到此結束,感謝看到此處。
歡迎大家糾錯和補充
如果覺得本篇文章對你有所幫助的話,不妨點個贊支持一下博主,拜托啦,這對我真的很重要。