目录
- 网络编程 套接字(socket)
- 1. 认识端口号
- 2. TCP协议
- 3. UDP协议
- 4. 网络字节序列
- 5. 常见的套接字
- 6. socket编程接口
- 6.1 socket常见API
- socket函数
- recvfrom函数
- sendto函数
- read函数 从tcp socket中读取接收数据
- 6.2 sockaddr结构
- 6.3 地址转换函数
- 6.4 udp socket实例
- 本地回环地址 127.0.0.1
- 6.5 tcp实例
网络编程 套接字(socket)
真正的网络通信过程,本质其实是进程间通信。 将数据在主机间转发仅仅是手段,机器收到之后,需要将数据交付给指定的进程。
因此,仅仅有ip地址是无法完成通信的,有了ip地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出,这个数据要给哪个程序进行解析。
IP地址+端口号称为套接字(SRC_IP+SRC_PORT -> 套接字),因此称为套接字编程。
1. 认识端口号
端口号,标识特定主机上的网络进程的唯一性。
端口号(port)是传输层协议的内容。
- 端口号是一个2字节16位的整数
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
- IP地址+端口号能够标识网络上的某一台主机的某一个进程
- 一个端口号只能被一个进程占用,一个进程可以绑定多个端口号。
不是所有的进程都需要端口号。
2. TCP协议
TCP(Transmission Control Protocol 传输控制协议)
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
3. UDP协议
UDP(User Datagram Protocol 用户数据报协议)
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
4. 网络字节序列
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>unit32_t htonl(unit32_t hostlong); //将32位的长整数从主机字节序转换为网络字节序
unit16_t htons(unit16_t hostshort); //将16位的短整数从主机字节序转换为网络字节序
unit32_t ntohl(unit32_t netlong); //将32位的长整数从网络字节序转换为主机字节序
unit16_t ntohs(unit16_t netshort); //将16位的短整数从网络字节序转换为主机字节序
- h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
5. 常见的套接字
- 域间socket (本主机内进程间通信,也被称为一种基于套接字接口的管道通信策略)
- 原始socket(不常见,通常用来编写工具,允许绕传输层直接使用网络层,或绕过其他直接使用底层)
- 网络socket
理论上,上面三种套接字,是三种应用场景,对应的应该是三套接口。
但实际上为了设计简单化,将所有的接口进行统一。
6. socket编程接口
网络通信的套接字的标准是基于process的。
6.1 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); //backlog:全链接队列长度
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);//成功返回一个整数,表示接受的socket
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 网络发送接口(tcp)
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
socket函数
**创建套接字的一个系统调用接口。**属于计算机网络提供的一个系统调用接口,是对传输层做了相关的一层文件系统级别的封装的接口。
成功返回创建的套接字文件描述符,失败返回-1。
- domain:指定协议族,常用的有AF_INET(IPv4)和AF_INET6(IPv6)。(网络通信or本地通信?)
- type:套接字类型,常用的有SOCK_STREAM(流套接字,提供可靠的、面向连接的通信)和SOCK_DGRAM(数据报套接字,提供不可靠的、无连接的通信)。
- protocol:指定协议,一般为0,表示根据domain和type自动选择合适的协议。
recvfrom函数
recvfrom
函数是用于接收数据的系统调用函数,它从udp套接字接收数据,并将其存储到指定的缓冲区中。recvfrom函数的返回值是一个整数,表示接收到的数据的字节数。
- 如果返回值大于0,表示成功接收到数据,并返回接收到的字节数。
- 如果返回值为0,表示对方已经关闭了连接。
- 如果返回值为-1,表示接收数据出现错误,可以通过检查errno来获取具体的错误原因。
sendto函数
发送数据到指定的目标地址
注意这里的addrlen不是指针,所以传的时候不用传地址。
read函数 从tcp socket中读取接收数据
同理,在tcp socket中,使用write进行写。
6.2 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6等,然而,各种网络协议的地址格式并不相同。
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结构体指针做为
参数。基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息:地址类型,端口号,IP地址。
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。
6.3 地址转换函数
字符串转in_addr
in_addr 转字符串
其中
inet_pton
和inet_ntop
不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr
。
6.4 udp socket实例
- 下面是一个简单的echo server例子。echo server:client给server发送消息,server原封不动返回
udp_server.hpp
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"#define SIZE 1024class UdpServer{
public:UdpServer(uint16_t port, std::string ip="0.0.0.0") :_port(port), _ip(ip), _sock(-1){}bool initServer(){//从这里开始,就是新的系统调用,来完成网络功能//1.创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0);if(_sock < 0){logMessage(FATAL, "%d : %s", errno, strerror(errno));exit(2);}//2. bind:将用户设置的ip和port在内核中和我们当前的进程强关联// "192.168.1.3" -> 点分十进制字符串风格的IP地址//每一个区域取值范围是[0-255]:1字节 2^8 -> 4个区域//点分十进制字符串风格的IP地址 <-> 4字节struct sockaddr_in local;bzero(&local, sizeof(local)); //当前字段清零local.sin_family = AF_INET;//ipv4 协议家族/域//服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络local.sin_port = htons(_port); //转参//1. 同上,点分十进制先要将字符串风格的IP地址-> 4字节IP//2. 4字节主机序列 -> 网络序列//有一套接口,可以一次帮我们做完这两件事情local.sin_addr.s_addr = inet_addr(_ip.c_str());//更改为从任意IP中获取信息//local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0){logMessage(FATAL, "%d : %s", errno, strerror(errno));exit(2); }logMessage(NORMAL, "init udp server done ... %s", strerror(errno));//donereturn true;}void Start(){//作为一款网络服务器,永远不退出的!所以是一个死循环//服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了/宕机//echo server:client给我们发送消息,我们原封不动返回char buffer[SIZE];for( ; ;){//注意://peer,纯输出型参数struct sockaddr_in peer;bzero(&peer, sizeof(peer));//输入:peer缓冲区大小//输出:实际读到的peersocklen_t len = sizeof(peer); //输入输出型参数//start 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(s > 0){buffer[s] = 0; //目前数据当做字符串uint16_t cli_port = ntohs(peer.sin_port); //peer是从网络中来的,需转换std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的点分十进制字符串风格的IP地址printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);}//分析和处理数据, TODO//end. 写回数据sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);}}~UdpServer(){if(_sock >= 0) close(_sock);}
private://一个服务器,一般必须需要ip地址和portuint16_t _port;std::string _ip;int _sock;
};
#endif
udp_server.cc
#include "udp_server.hpp"
#include <memory>static void usage(std::string proc){std::cout << "\nUsage: " << proc << "ip port\n" << std::endl;
}
int main(int argc, char *argv[]){if(argc != 3){usage(argv[0]);exit(1);}std::string ip = argv[1];uint16_t port = atoi(argv[2]);std::unique_ptr<UdpServer> svr(new UdpServer(port, ip));svr->initServer();svr->Start();return 0;
}
完善client端代码。
client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>static void usage(std::string proc){std::cout << "\nUsage: " << proc << "serverIp serverPort\n" << std::endl;
}
int main(int argc, char *argv[]){if(argc != 3){usage(argv[0]);exit(1);}//创建套接字int sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "socket error" << std::endl;exit(2);}//client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候选择)std::string message;struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(atoi(argv[2]));server.sin_addr.s_addr = inet_addr(argv[1]);char buffer[1024];while(true){std::cout << "请输入你的信息# ";std::getline(std::cin, message);//当client首次发送消息给服务器的时候,OS会自动给client bind它的IP和PORTsendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl;}}close(sock);return 0;
}
测试结果:
以上就实现了本地间的通信。
通过
netstat
查看网络连接等信息。
本地回环地址 127.0.0.1
127.0.0.1
是一个特殊的 IP 地址,被称为本地回环地址(loopback address)。它通常用于本机测试和网络通信的目的。当你在计算机上使用
127.0.0.1
作为目标地址时,数据将被发送到本机的网络堆栈,而不会通过网络接口发送出去。这使得你可以在本机上模拟网络通信,而无需实际连接到外部网络。本地回环地址
127.0.0.1
在 IPv4 中被保留,而在 IPv6 中,本地回环地址是::1
。在网络编程中,可以使用
127.0.0.1
来测试和调试网络应用程序,或者在本地搭建服务器和客户端进行通信。
- 通过popen模拟shell,client发出命令,server处理并返回执行结果
void Start(){//作为一款网络服务器,永远不退出的!所以是一个死循环//服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了/宕机//echo server:client给我们发送消息,我们原封不动返回char buffer[SIZE];for( ; ;){//注意://peer,纯输出型参数struct sockaddr_in peer;bzero(&peer, sizeof(peer));//输入:peer缓冲区大小//输出:实际读到的peersocklen_t len = sizeof(peer); //输入输出型参数char result[256];std::string cmd_echo;//start 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(s > 0){buffer[s] = 0; //目前数据当做字符串if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr){std::string err_message = "坏人...."; std::cout << err_message << buffer << std::endl;sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr*)&peer, len);continue;}FILE *fp = popen(buffer, "r");if(nullptr == fp){logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));continue;}while (fgets(result, sizeof(result), fp) != nullptr){cmd_echo += result;}fclose(fp);}//分析和处理数据, TODO//end. 写回数据// sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr*)&peer, len);}}
}
- 使用多线程实现客户端之间通讯
client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <memory>
#include "thread.hpp"uint16_t serverport = 0;
std::string serverip;// int sock = -1;
static void usage(std::string proc){std::cout << "\nUsage: " << proc << "serverIp serverPort\n" << std::endl;
}static void *udpSend(void *args){int sock = *(int*)((ThreadData*)args)->args_; //线程套接字std::string name = ((ThreadData*)args)->name_; //线程名std::string message;struct sockaddr_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());//不断进行发送while (true){std::cerr << "请输入你的信息# "; //标准错误 2打印std::getline(std::cin, message);if(message == "quit")break;//当client首次发送消息给服务器的时候,OS会自动给client bind它的IP和PORTsendto(sock, message.c_str() , message.size(), 0, (struct sockaddr*)&server, sizeof(server));}return nullptr;
}
static void *udpRecv(void *args){int sock = *(int*)((ThreadData*)args)->args_; //线程套接字std::string name = ((ThreadData*)args)->name_; //线程名char buffer[1024];while(true){//初始化为0memset(buffer, 0, sizeof(buffer));struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);if(s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}}int main(int argc, char *argv[]){if(argc != 3){usage(argv[0]);exit(1);}//创建套接字int sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "socket error" << std::endl;exit(2);}//初始化port和ipserverport = atoi(argv[2]);serverip = argv[1];/*线程编号,回调方式,传入套接字*/std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void*)&sock));std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void*)&sock));// sender->name();sender->start();recver->start();sender->join();recver->join();close(sock);return 0;
}
udp_server.hpp
void Start(){//作为一款网络服务器,永远不退出的!所以是一个死循环//服务器启动 -> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了/宕机//echo server:client给我们发送消息,我们原封不动返回char buffer[SIZE];for( ; ;){//注意://peer,纯输出型参数struct sockaddr_in peer;bzero(&peer, sizeof(peer));//输入:peer缓冲区大小//输出:实际读到的peersocklen_t len = sizeof(peer); //输入输出型参数char result[256];char key[64];std::string cmd_echo;//start 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(s > 0){buffer[s] = 0; //目前数据当做字符串uint16_t cli_port = ntohs(peer.sin_port); //peer是从网络中来的,需转换std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的点分十进制字符串风格的IP地址//简单的用户托管snprintf(key, sizeof key, "%s-%d", cli_ip.c_str(), cli_port); // 127.0.0.1-8080logMessage(NORMAL, "key: %s", key);auto it = _users.find(key);if(it == _users.end()){//not exitslogMessage(NORMAL, "add new user : %s", key);_users.insert({key, peer});}}for(auto &iter : _users){/*key,即前面的用ip和port来标识一个用户的身份*/std::string sendMessage = key;sendMessage += "#";sendMessage += buffer; //127.0.0.1-1234# 你好logMessage(NORMAL, "push message to %s", iter.first.c_str());sendto(_sock, sendMessage.c_str(), sendMessage.size(),0, (struct sockaddr *)&(iter.second), sizeof(iter.second));}}
}
注意:
-
云服务器无法直接绑定公网IP或 非127.0.0.1、 非0.0.0.0这样的IP,即云服务器无法绑定公网IP。
-
对于服务器来讲,不推荐绑定一个确定的IP,推荐使用绑定任意IP的方案。 即让服务器在工作过程中,可以从指定端口中的任意IP中获取数据。
-
无论是多线程读还是写,用的sock都是一个,sock代表一个文件,可以同时进行收发的原因
- UDP是全双工的,可以同时进行收发而不受干扰。
6.5 tcp实例
可以通过对回调函数service的不同实现,来实现server端对client端的通信返回内容。下面是三个对回调函数不同实现的例子。
static void service( ); //简单的echoserver
static void change( ); //对于client发送到server的所有小写字母,转换为大写字母,再返回
static void dictOnline( ); //实现一个小词典
同样,当获取连接成功时,也列举了四种方式进行通信服务。
tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <memory>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "ThreadPool/Task.hpp"
#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include <netinet/in.h>// // 接收到客户端数据后进行的处理
static void service(int sock, const std::string &clientip,const uint16_t &clientport, const std::string &thread_name)
{// echo serverchar buffer[1024];while (true){// read && write 可以直接被使用ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0; // 将对应的发过来的数据当作字符串std::cout << thread_name << "|" << clientip << ":" << clientport << "#" << buffer << std::endl;}else if (s == 0) // 读取到返回值为0,代表对端关闭连接{logMessage(NORMAL, "%s : %d shutdown, me too!", clientip.c_str(), clientport);break;}else // 异常{logMessage(FATAL, "read socket error%d:%s", errno, strerror(errno));break;}// 读取成功,继续向socket写入write(sock, buffer, strlen(buffer));}close(sock);
}static void change(int sock, const std::string &clientip,const uint16_t &clientport, const std::string &thread_name)
{// 大小写转换char buffer[1024];// read && write 可以直接被使用ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0; // 将对应的发过来的数据当作字符串std::cout << thread_name << "|" << clientip << ":" << clientport << "#" << buffer << std::endl;std::string message;char *start = buffer;while (*start){char c;if (islower(*start))c = toupper(*start);elsec = *start;message.push_back(c);start++;}write(sock, message.c_str(), message.size());}else if (s == 0) // 读取到返回值为0,代表对端关闭连接{logMessage(NORMAL, "%s : %d shutdown, me too!", clientip.c_str(), clientport);}else // 异常{logMessage(FATAL, "read socket error%d:%s", errno, strerror(errno));}// 读取成功,继续向socket写入write(sock, buffer, strlen(buffer));close(sock);
}static void dictOnline(int sock, const std::string &clientip,const uint16_t &clientport, const std::string &thread_name)
{// echo serverchar buffer[1024];static std::unordered_map<std::string, std::string> dict = {{"apple", "苹果"},{"bite", "比特"},{"banana", "香蕉"},{"hard", "好难啊"}};// read && write 可以直接被使用ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0; // 将对应的发过来的数据当作字符串std::cout << thread_name << "|" << clientip << ":" << clientport << "#" << buffer << std::endl;std::string message;auto iter = dict.find(buffer);if(iter == dict.end()) message = "我不知道...";else message = iter->second;write(sock, message.c_str(), message.size());}else if (s == 0) // 读取到返回值为0,代表对端关闭连接{logMessage(NORMAL, "%s : %d shutdown, me too!", clientip.c_str(), clientport);}else // 异常{logMessage(FATAL, "read socket error%d:%s", errno, strerror(errno));}// 读取成功,继续向socket写入close(sock);
}
// class ThreadData{
// public:
// int _sock;
// std::string _ip;
// uint16_t _port;// };class TcpServer
{
private:const static int gbacklog = 20; // 一般不能太大也不能太小// static void *threadRoutine(void *args){// //线程分离// pthread_detach(pthread_self());// ThreadData *td = static_cast<ThreadData *>(args);// service(td->_sock, td->_ip, td->_port);// delete td;// }public:TcpServer(uint16_t port, std::string ip = "0.0.0.0"): _listensock(-1), _port(port), _ip(ip), _threadpool_ptr(ThreadPool<Task>::getThreadPool()){}void initServer(){// 1. 创建套接字 -- 进程和文件(以进程方式打开了一个文件)_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){logMessage(FATAL, "create socket error%d:%s", errno, strerror(errno));exit(2);}// 打印出创建的套接字,此处为流式logMessage(NORMAL, "create socket success, listensock: %d", _listensock);// 2. bind -- 文件 + 网络struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);// local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));exit(3);}// 3. 因为tcp是面向连接的,当正式通信的时候,需要先建立连接if (listen(_listensock, gbacklog) < 0){logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init server success!");}void Start(){// signal(SIGCHLD, SIG_IGN); //主动忽略SIGCHLD信号,子进程退出的时候,自动退出僵尸状态// SIGCHLD:子进程终止或停止时,父进程收到的信号_threadpool_ptr->run(); // push任务while (true){// sleep(1);// 4. 获取链接struct sockaddr_in src;socklen_t len = sizeof(src);//(servicesock)通过每一次获得的新连接进行IO vs (_sock)j监听socket:获取新连接int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);if (servicesock < 0){// 获取连接失败logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));continue;}// 获取连接成功uint16_t client_port = ntohs(src.sin_port);std::string client_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link success, servicesock: %d | %s : %d | \n",servicesock, client_ip.c_str(), client_port);// 开始进行通信服务// version4 --线程池版本Task t(servicesock, client_ip, client_port, dictOnline);_threadpool_ptr->pushTask(t);// version3 --多线程版本// ThreadData *td = new ThreadData();// td->_sock = servicesock;// td->_ip = client_ip;// td->_port = client_port;// pthread_t tid;// pthread_create(&tid, nullptr, threadRoutine, td);// version2.1 --多进程版,不使用signal// pid_t id = fork();// if(id == 0){// //child// close(_listensock);// if(fork() > 0) exit(0); //子进程本身立即退出// //孙子进程被os接管// service(servicesock, client_ip, client_port);// exit(0);// }// //parent// waitpid(id, nullptr, 0); //因为上面的子进程直接退出,所以不会造成阻塞// close(servicesock);// version 1 -- 单进程循环版 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个// 很显然,是不能够直接被使用的// service(servicesock, c lient_ip, client_port);// version 2 -- 多进程版 -- 创建子进程// 让子进程给新的连接提供服务,子进程能打开父进程曾经打开的文件fd// pid_t id = fork();// assert(id != -1);// if(id == 0){// //子进程// //子进程会继承父进程打开的文件与文件fd,因此此时需要关闭不需要的套接字// close(_listensock);// service(servicesock, client_ip, client_port);// exit(0);// }// 父进程// close(servicesock); //不关闭会造成文件描述符泄漏}}~TcpServer() {}private:uint16_t _port;std::string _ip;int _listensock;std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};
tcp_client.cc
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>static void usage(std::string proc)
{std::cout << "\nUsage: " << proc << " serverIP serverPort\n"<< std::endl;
}// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);bool alive = false;int sock = 0;std::string line;while (true){if (!alive){sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket error" << std::endl;exit(2);}// 此处客户端依然不需要bind,不需要显示的bind,但是一定是需要port// 需要让OS自动进行port选择// 要有连接别人的能力struct sockaddr_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());if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0){std::cerr << "connect error" << std::endl;exit(3);}std::cout << "connect success" << std::endl;alive = true;}std::cout << "请输入# ";std::getline(std::cin, line);if (line == "quit")break;ssize_t s = send(sock, line.c_str(), line.size(), 0);if (s > 0){char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;std::cout << "server 回显# " << buffer << std::endl;}else if (s == 0){alive = false;close(sock);}}else{alive = false;close(sock);}}return 0;
}