目录
网络传输本质
认识端口号
认识协议
认识TCP协议
认识UDP协议
网络字节序
socket编程接口
socket 常见API
sockaddr结构
sockaddr 结构
sockaddr_in 结构
in_addr结构
编写代码前的小tip(重点)
UDP套接字(网络通信)代码
Linux
Log.hpp
makefile
udpServer.cc
udpClient.cc
Windows
TCP套接字(网络通信)代码
Linux
Log.hpp
makefile
tcpServer.cc
tcpClient.cc
Windows
C语言总结在这常见八大排序在这
作者和朋友建立的社区:非科班转码社区-CSDN社区云
期待hxd的支持哈
最后是打鸡血环节:想多了都是问题,做多了都是答案
最近作者和好友建立了一个公众号
公众号介绍:
专注于自学编程领域。由USTC、WHU、SDU等高校学生、ACM竞赛选手、CSDN万粉博主、双非上岸BAT学长原创。分享业内资讯、硬核原创资源、职业规划等,和大家一起努力、成长。(二维码在文章底部哈!)
主机间在通信的本质其实是两主机的两个进程在相互通信。
IP地址可以完成主机与主机间的通信,而主机上各自的通信进程,才是发送和接收数据的一方。
我们在网络通信的时候,那便是不能只考虑两台主机交换数据,还要考虑到进程和进程,本质上讲,进行数据交互的时候,是用户和用户在进行交互,用户的身份,通常是程序的体现!(程序一定是在运行中 —— 进程)
那么 IP —— 确保主机的唯一性
Port(端口号) —— 确保该主机上的进程的唯一性
--》IP + Port 标识互联网中唯一的进程!
我们把这种IP+端口(Pork)称为socket(插座,代表的是配套的(插头配插座))(属于POSIX的进程间通信,跨网络的)。网络通信的本质也就是进程间通信。
我们知道进程间通信的本质就是要看到同一份资源,在网络中这份资源就是网络。
端口号 (port) 是传输层协议的内容:
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用
传输层协议 (TCP 和 UDP) 的数据段中有两个端口号, 分别叫做源端口号和目的端口号, 还有两个是源IP和目的IP,源端口和源IP是一对socket,另一个也是一对,这两个socket对一对接我们就知道哪两台主机的哪两个服务要通信就知道了PS:OS里面有两张重要的哈希表,一个是PID做key的,一个是端口号做key的。
认识协议
认识TCP协议
此处我们先对 TCP(Transmission Control Protocol 传输控制协议 ) 有一个直观的认识 ; 后面我们再详细讨论 TCP 的一些细节问题。
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
认识UDP协议
此处我们先对 TCP(Transmission Control Protocol 传输控制协议 ) 有一个直观的认识 ; 后面我们再详细讨论 TCP 的一些细节问题。
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
有连接:比如打电话,要拿起手机拨通对方电话号码然后两个人确定对方能听到这些准备工作就是建立连接的过程。
无连接:比如发邮件,我只要知道你的邮箱就可以直接给你发了,并不需要你的许可。
可靠/不可靠传输:
并不是靠不靠谱的意思,更多是指通信特征,没有任何主观色彩。
可靠传输:可靠传输就比如保证按需到达,保证数据丢包的时候重传,比如数据重复时去重等等。
不可靠传输:我直接把数据发给你,你爱收不收,我什么都不管。
所以不要去衡量tcp udp哪一个好了,只有哪一个更合适。就比如tcp要维持可靠传输就肯定要做更多的工作。而udp就是什么都不管,上层把数据丢给我我直接就是丢给下层(添加自己的报头告诉对方谁发的,什么时候发的...),但是就说明我不需要做更多的工作,我一定是足够简单的。这样不同的场景就需要不同的通信方式,就比如支付的时候就一定要有保证(保证可靠传输),就要用tcp。比如看视频,看直播,如果同tcp这样复杂的协议,就会导致直播转播方的压力倍增,如果网络卡顿,那就是直接卡掉线要重新建立连接,所以直接使用udp协议来进行,不用有连接,直接有数据包发给用户,网好就看,网不好就是卡顿。
我们已经知道 , 内存中的多字节数据相对于内存地址有大端和小端之分 , 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分 . 那么如何定义网络数据流的地址呢 ?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出 ;接收主机把从网络上接到的字节依次保存在接收缓冲区中 , 也是按内存地址从低到高的顺序保存 ;因此 , 网络数据流的地址应这样规定 : 先发出的数据是低地址 , 后发出的数据是高地址 .TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.不管这台主机是大端机还是小端机 , 都会按照这个 TCP/IP 规定的网络字节序来发送 / 接收数据 ;如果当前发送主机是小端 , 就需要先将数据转成大端 ; 否则就忽略 , 直接发送即可 ;在我们之前就学了大小端,但是我们发现一直是没有用上,大小端的问题其实就是在网络里面体现出来的,因为不同主机如果字节序列不同,数据发送过来就有可能将大端机的数据小端机去接受就可能解释数据的时候就解释反了!所以为了处理这个问题,就规定网络数据都是大端序列(默认),也就是说主机发送的数据要去主动或者被动去转换为大端序列。(为什么网络发送数据采用大端序列,有一种猜测是:大端是把高权值数据放在低地址出,而发送数据又是从低地址发的,所以如果发送0x1234,那么拿数据的时候就是先拿高权值的数据1,再234,这样就方便拿数据!
(sum= 1* 10+ 2* 10 + 3* 10 +4)为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
socket 常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器) int socket(int domain, int type, int protocol); 三个参数,一个使用ipv4 第二个是用户数据报通信,第三个0,这样的只有一个就是udp 往后只用这两个套接字类型,域就ipv4(AF_INET),第三个参数为0 // 绑定端口号 (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); accept 返回一个新的socket套接字 (为什么会有两类套接字) 第二个参数就是让服务器知道是谁连的我,第三个是长度 // 建立连接 (TCP, 客户端) int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
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结构体指针做为参数;
我们的套接字通信(API),设计的时候除了要支持网络通信还要支持本地通信,所以网络套接字在网络中划分为两类,一类是网络通信的我们称为跨网络套接字,一般也就是套接字(默认就是网络通信)。还有一种是域间套接字(代表的就是在本地直接通信),也称为双向管道(之前文件的管道是单向的)(目前不讲,因为相比于网络的API没有端口号没有ip的概念会更简单,而且又是管道)。但是设计者为了把这两种通信方式统一起来并且以文件的方案让我们去使用,但是因为本地通信和网络通信要的数据是不一样的(本地不要ip,端口,只是看到同一份资源就可以了),所以设计者就设计了一种抽象结构,in-->inet 表示网络,表示ipv4 ipv6这样的协议,un-->unix简写,代表我们的域间套接字。为了统一可以调用两种结构,就肯定不能是他们两个,所以就有了 struct sockaddr,这个类型里其他有效字段基本没有,但是他的第一个字段,前16位都是一样的,当你传后两种的时候,要把他们两个的类型强转为 struct sockaddr,然后 struct sockaddr 并不知道你传的是哪个,但是没关系,直接拿着前165位去匹配,如果他是AF_INET,那么就可以把他当作(内部强转为struct sockaddr_in,再去用这个结构体去访问内部数据,域间通信一样)是网络通信(就提端口号和ip),是AF_UNIX就是域间通信(就提路径),这就像是多态。
sockaddr 结构
sockaddr_in 结构
虽然 socket api 的接口是 sockaddr, 但是我们真正在基于 IPv4 编程时 , 使用的数据结构是 sockaddr_in; 这个结构里主要有三部分信息: 地址类型 , 端口号 , IP 地址。in_addr结构
1. ifconfig,如果使用云服务器,那么看见的是内网ip,如果是虚拟机就是公网ip,绑定这个就可以。
2. 关于端口号,不要绑定0-1023以前的,服务器中的端口号分配是有他自己的规则的,比如http是80,https是43,Mysql对应的网络服务是3306等。
3. netstat 查看网络服务
允许远端的任何主机的任何端口发消息 (0.0.0.0)
4. recvfrom(sendto)(udp特有的)(定长数据) 最后两个参数是输出型参数,是谁(客户端)给你发的消息和长度,会把客户端信息保存到RmtPort(远端)中 。
5. 127.0.0.1:本地环回
客户端发的消息经过网络协议栈,但是不往网络里发,直接到了网络协议栈最底部,然后再由最底部向上交付,再交付给另一个进程他对应的缓冲区里面,让他读到消息。所以一般(127.0.0.1)就代表本主机。
(PS:如果要把写的客户端发给别人就makefile里面静态编译)
6. sz name 可以把文件发到桌面
7. rz -e 可以把桌面的文件放到linux中
8. (这点可后面再看)直接客户端用的 server_ip 是云服务器的就可以直接向server发消息
9. (这点可后面再看)作为一个udpserver,那么他是一个网络服务,那么我们是不是应该让linux作为服务端,windows作为客户端。也就是说把我们的客户端写在windows下,让我们的linux和windows直接通信。其实,windows和linux的套接字接口是一样的!只有开头一点点不一样!
首先加上这个库
然后初始化启动信息和创建套接字
直接调用WSAStarup,这就是相当于初始化我们的套接字资源,只不过是库去初始化的,然后传入套接字的版本信息,然后初始话的信息写道上面的date。创建套接字就是把原来代码的这个地方改了就可以了
然后就是windows下没有命令行就把之前要用的port和ip直接定义出来
然后就可以了!然后就可以直接把.exe文件发给别人了!
然后是一些零零散散的函数
UDP套接字(网络通信)代码
Linux
Log.hpp
#pragma once #include
#include #include void LogMessage(std::string ip, __uint16_t port, std::string message = "nullptr") { std::cout << "[ip:" << ip << "][port:" << port << "]" << ": " << message << std::endl; } makefile
.PHONY:all all:udpClient udpServer udpClient:udpClient.cc g++ -o $@ $^ -std=c++11 -lpthread udpServer:udpServer.cc g++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean: rm -rf udpClient udpServer
udpServer.cc
#include "Log.hpp" #include
#include #include #include // #include // #include // ! 这两货从哪来 tip:inet_addr #include // ! #include #include #include #include #define BUFFER_SIZE 1024 class UdpServer; //先声明 static void Usage(const std::string porc) { std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl; } class ThreadData { public: uint16_t clientPort_; std::string clinetIp_; int sock_; UdpServer *this_; public: ThreadData(uint16_t port, std::string ip, int sock, UdpServer *ts) : clientPort_(port), clinetIp_(ip), sock_(sock), this_(ts) { } }; // ./UdeServer port [ip] class UdpServer { public: UdpServer(int port, std::string ip = "") : _port((uint16_t)port), _ip(ip), _socket(-1) { } ~UdpServer() { } public: //初始化服务器 void init() { // 1. 创建 soket 套接字 : domain type protocol _socket = socket(AF_INET, SOCK_DGRAM, 0); if (_socket < 0) { std::cerr << __LINE__ << ":socket err" << std::endl; exit(1); } // 2. 绑定网络信息,指名ip+port // 2.1首先填充基本信息到 struct sockaddr_in struct sockaddr_in local; bzero(&local, 0); //填充协议家族 域(网络通信还是本地通信) local.sin_family = AF_INET; // PF_INET一样的 本地通信还是网络通信 //填充服务器对应的端口号信息,一定是会发送给对方的,_port一定会到网络中,所以htons local.sin_port = htons(_port); //服务器都有ip地址,"15,23,45,888",字符串风格的点分十进制 -> 因为每小段都是0-255所以每小段都是一个字节就可以了 //所以要转化为 四字节ip -> uint32_t ip // INADDR_ANY(本质是0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法 // inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n // 云服务器有一些特殊情况: // 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意 local.sin_addr.s_addr = _ip.empty() ? htonl(INADDR_ANY) : inet_addr(_ip.c_str()); // 2.2bind 网络信息 因为之前的local是在用户栈创建的,是临时变量,现在要把他写入内核中 if (bind(_socket, (sockaddr *)&local, sizeof(local)) < 0) { std::cerr << __LINE__ << "bind err" << std::endl; exit(2); } std::cout << "socket bind success : " << _socket << std::endl; std::cout << "creat server success !" << std::endl; } // 大小写转化服务 // TCP && UDP: 支持全双工 void transService(std::string &inbuffer,int s) { std::cout << "into transService!" << std::endl; // char inbuffer[BUFFER_SIZE]; std::cout << "Uper before:" << inbuffer << std::endl; for (int i = 0; i < s; i++) { if (isalpha(inbuffer[i]) && islower(inbuffer[i])) inbuffer[i] = toupper(inbuffer[i]); } std::cout << "Uper after:" << inbuffer << std::endl; } // static void *threadRoutine(void *args) // { // std::cout << "into thread" << std::endl; // pthread_detach(pthread_self()); //设置线程分离 // ThreadData *td = static_cast (args); // // ThreadData* td=(ThreadData*)args; // //然后是线程去执行服务 // td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_); // delete td; // return nullptr; // } //启动服务器 void start() { // 服务器设计的时候,服务器都是死循环 std::cout << "开始启动服务器" << std::endl; std::string inbuffer; inbuffer.resize(1024); // char inbuffer[1024]; // char outbuffer[1024]; while (true) { struct sockaddr_in RmtPort; // RmtPort 远端 socklen_t len = sizeof(RmtPort); // UDP无连接的 // 对方给你发了消息,你想不想给对方回消息?要的!后面的两个参数是输出型参数,代表谁给你发的 ssize_t s = recvfrom(_socket, (void *)inbuffer.c_str(), sizeof(inbuffer) - 1, 0, //阻塞等待 (struct sockaddr *)&RmtPort, &len); if (s > 0) { std::cout << "s>0" << std::endl; inbuffer[s] = '\0'; //当作字符串,因为上面-1了的就是给'\0'预留位置 } else if (s == -1) { std::cout << __LINE__ << " : recvfrom warning" << std::endl; std::cout << strerror(errno) << std::endl; // pos continue; //因为服务器不能退出 } //到这就读取成功了 std::string RmtPortIp = inet_ntoa(RmtPort.sin_addr); uint16_t RmtPortPort = RmtPort.sin_port; LogMessage(RmtPortIp, RmtPortPort, "server read client success"); std::cout << "[ip:" << RmtPortIp << "][port:" << RmtPortPort << "]" << ": " << inbuffer << std::endl; // //开始提供服务 // ThreadData *td = new ThreadData(RmtPortPort, RmtPortIp, _socket, this); // pthread_t tid; // pthread_create(&tid, nullptr, threadRoutine, (void *)td); // std::cout << "endl...." << std::endl; transService(inbuffer,s); sendto(_socket, inbuffer.c_str(), s, 0, (struct sockaddr *)&RmtPort, len); std::cout << "send to cliet[ip:" << RmtPortIp << "]"<< "[port:" << RmtPortPort << "]" << std::endl; } } private: //服务器必须要有端口号信息 uint16_t _port; //服务器必须要有ip地址 std::string _ip; //服务器的 socket fd 信息 int _socket; }; int main(int argc, char *argv[]) { if (argc != 2 && argc != 3) //反面:argc == 2 || argc == 3 { Usage(argv[0]); exit(3); } uint16_t port = atoi(argv[1]); std::string ip; if (argc == 3) { ip = argv[2]; } UdpServer udpser(port, ip); udpser.init(); udpser.start(); return 0; } udpClient.cc
#include
#include #include #include #include #include //网络四必须包 #include #include #include #include // #include #include using std::cout; using std::endl; void Usage(std::string name) { cout << "Usage:\n\t" << name << " ip port" << endl; } void *recverServer(void *arg) { cout<<"into thread"< 0) { cout << "server echo#" << buffer << endl; } } return nullptr; } // ./udpClient server_ip server_port // 如果一个客户端要连接server必须知道server对应的ip和port int main(int args, char *argv[]) { if (args != 3) { Usage(argv[0]); exit(1); } // 1. 根据传入的参数,得到要访问服务器数据 std::string severr_ip = argv[1]; uint16_t server_port = atoi(argv[2]); // 2. 创建客户端 // 2.1创建Socket int 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; bzero(&server, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(server_port); server.sin_addr.s_addr = inet_addr(severr_ip.c_str()); //创建线程去接受服务器的消息 pthread_t t; pthread_create(&t, nullptr, recverServer, (void *)&sockfd); // 3. cilent 向 server 通信 std::string buffer; while (true) { cout << "Please Entet: "; getline(std::cin, buffer); //发送消息给服务器 sendto(sockfd,buffer.c_str(),buffer.size(),0, (const struct sockaddr*)&server,sizeof(server)); } return 0; } Windows
额..........网上是有的,上面也已经提到了,我相信以大家的实力也不用我贴代码了吧,虽然我开始是想写的,但是我懒...或者说写这篇文章快把我整郁闷了(bug真的多),等之后再有想法了我再回来补上吧=。=。
TCP套接字(网络通信)代码
1. 创建套接字,只要是网络的,流式的,就默认是tcp
面向什么就是说要做什么之前,要先有面向的对象。比如面向连接就是做其他事情之前要先有连接。(tcp是面向连接的)2. 然后读取和写入因为都是流式的所有就可以用write和read(tcp)
对应recvfrom和sendto是不能用的,因为这个是专门对于udp发送固定大小的报文
3. 文件描述符打开的个数是有上限的。每个进程都对应有一个文件描述符表,这个文件描述符表默认的在云服务器,非生产环境的这种机器中一般一个进程能打开的文件描述符数一般是32/64,而云服务器他的一个进程能打开的文件描述符数是10w/65535/65536,所以如果打开了不归还,就是文件描述符泄漏。(满了就无法建立新连接,这个服务器就无法对外提供服务了)
4. 我们一直是对于同一个sock进行write和read(udp也是),证明了 TCP:UDP支持全双工。
5. 如何让自己的程序只启动一次,bind端口号就是一种方法(因为你占用了别人就不能再去启动了,因为端口没有了) 。
6. 正常退出后client再连,然后ctrl+c程序异常退出,server端也可以知道client退出了,然后关闭文件描述符。
7. 注意getline结尾不会读到‘\n’。
注意:
当运行自己服务器的时候
这个是公网ip,是云服务上的,并不是我们自己的,所以不能用这个ip去做服务器,但是别人访问我们的时候,是要用这个公网ip来访问我们,他是会转换为我们linux下的自己的内网ip,这个才是我们自己的。
所以,我们自己运行服务器的时候,也要用自己的内网ip。
Linux
Log.hpp
#include
#include #include #include #include #include #include using std::cout; using std::endl; #define DEBUG 0 #define NOTICE 1 #define WARINING 2 #define FATAL 3 const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"}; // logMessage(DEBUG, "exaple xxx", strerror(errno)); void logMessage(int level, std::string message = "unmessage") { assert(level >= DEBUG); assert(level <= FATAL); FILE *out = (level == FATAL ? stderr : stdout); std::string errmsg = "unerr"; if (errno != 0) { errmsg = strerror(errno); } fprintf(out, "%s | %u | line: %d | %s | %s \n", log_level[level], (unsigned int)time(NULL), __LINE__, message.c_str(), errmsg.c_str()); } //#pragma once // #include // #include // #include // #include // #include // #include // #include // #define DEBUG 0 // #define NOTICE 1 // #define WARINING 2 // #define FATAL 3 // const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"}; // //logMessage(DEBUG, "exaple xxx", strerror(errno)); // void logMessage(int level, const char *format, ...) // { // assert(level >= DEBUG); // assert(level <= FATAL); // char *name = getenv("USER"); // char logInfo[1024]; // va_list ap; // ap -> char* // va_start(ap, format); // vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap); // va_end(ap); // ap = NULL // FILE *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; // // } // } makefile
.PHONY:all all:tcpClient tcpServer tcpClient:tcpClient.cc g++ -o $@ $^ -std=c++11 tcpServer:tcpServer.cc g++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean: rm -rf tcpClient tcpServer
tcpServer.cc
#include
#include #include #include #include // #include #include // 咋找这个头文件! #include #include // #include "Log.hpp" #include using std::cout; using std::endl; class ServerTcp; struct ThreadDate { public: ThreadDate(int sock, std::string ip, uint16_t port, ServerTcp *portthis) : listensocket_(sock), PortIp_(ip), PortPt_(port), this_(portthis) {} int listensocket_; std::string PortIp_; uint16_t PortPt_; ServerTcp *this_; }; // ./ServerTcp local_port [local_ip] class ServerTcp { public: ServerTcp(uint16_t port, std::string ip = "") : listensocket_(-1), port_(port), ip_(ip) { } ~ServerTcp() { } public: void init() { // 1. 创建socket listensocket_ = socket(PF_INET, SOCK_STREAM, 0); if (listensocket_ < 0) { logMessage(FATAL, "server socket err"); exit(-1); } logMessage(DEBUG, "server socket success!"); // 2. bind绑定 int bind(int socket, const struct sockaddr *address,socklen_t address_len); // 2.1 填充服务器信息 struct sockaddr_in local; bzero(&local, sizeof(local)); // 初始化 local.sin_family = PF_INET; // 网络通信 local.sin_port = htons(port_); // 端口号 local.sin_addr.s_addr = (ip_.empty() ? INADDR_ANY : local.sin_addr.s_addr = inet_addr(ip_.c_str())); // IP socklen_t len = sizeof(local); // 2.2 本地socket信息,写入sock_对应的 内核 区域 int ret = bind(listensocket_, (const struct sockaddr *)&local, len); if (ret < 0) { logMessage(FATAL, "server bind err"); exit(-1); } logMessage(DEBUG, "server bind success!"); // 3. 监听socket,为何要监听呢?因为tcp是面向连接的! ret = listen(listensocket_, 5 /*这个后面会讲是什么*/); if (ret < 0) { logMessage(FATAL, "server listen err"); exit(-1); } logMessage(DEBUG, "server listen success!"); } void start() { while (true) { logMessage(DEBUG, "The server is running properly"); // 4. 获取连接, accept 的返回值是一个新的socket fd?!!! // 这个fd是专门给Clien去提供服务的 // init里面的是监听socket,是专门去招Client的 struct sockaddr_in remotP; // 远端/客户端 socklen_t len = sizeof(remotP); int serviceSock = accept(listensocket_, (struct sockaddr *)&remotP, &len); if (serviceSock < 0) { logMessage(FATAL, "server accept err"); exit(-1); } logMessage(DEBUG, "server accept success!"); // 4.1 获取客户端基本信息 uint16_t ClientPort = ntohs(remotP.sin_port); std::string ClientIp = inet_ntoa(remotP.sin_addr); cout << "ClientPort: " << ClientPort << " | ClientIp: " << ClientIp << endl; // 5. 提供服务, echo -> 小写 -> 大写(多个版本) ThreadDate *td = new ThreadDate(serviceSock , ClientIp, ClientPort, this); pthread_t tid; pthread_create(&tid, NULL, threadRoutine, td); } } static void *threadRoutine(void *argv) { pthread_detach(pthread_self()); // 设置线程分离 cout<<"pthread detach"< (argv); td->this_->transService(td->listensocket_,td->PortIp_,td->PortPt_); return NULL; } #define BUFFER_SIZE 1024 void transService(int sock, const std::string &clientIp, uint16_t clientPort) { std::cout << "into transService!" << std::endl; char inbuffer[BUFFER_SIZE]; while (true) { ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); // 我们认为我们读到的都是字符串 if (s > 0) { // read success inbuffer[s] = '\0'; if (strcasecmp(inbuffer, "quit") == 0) { logMessage(DEBUG, "client quit"); break; } std::cout << "Uper before:" << inbuffer << std::endl; // 可以进行大小写转化了 for (int i = 0; i < s; i++) { if (isalpha(inbuffer[i]) && islower(inbuffer[i])) inbuffer[i] = toupper(inbuffer[i]); } std::cout << "Uper after:" << inbuffer << std::endl; write(sock, inbuffer, strlen(inbuffer)); } else if (s == 0) { // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭 // s == 0: 代表对方关闭,client 退出 logMessage(DEBUG, "client quit"); break; } else { logMessage(FATAL, "read err"); break; } } } private: // 服务器 sockt int listensocket_; // 服务器 port uint16_t port_; // 服务器 ip std::string ip_; }; void Usage(char *name) { cout << name << " port [ip]" << endl; } // int main() // { // short port = htons(8080); // std::string ip = "0.0.0.0"; // ServerTcp server(port, ip); // server.init(); // server.start(); // return 0; // } //./ServerTcp local_port [local_ip] int main(int argc, char *argv[]) { if (argc != 2 && argc != 3) { Usage(argv[0]); exit(-1); } uint16_t port = atoi(argv[1]); // 注意转一下 std::string ip; if (argc == 3) ip = argv[2]; ServerTcp server(port, ip); server.init(); server.start(); return 0; } tcpClient.cc
#include "Log.hpp" #include
#include #include // #include #include #include #include // #include using std::cout; using std::endl; bool quit = false; class TcpClient { public: TcpClient(std::string serverIp, uint16_t serverPort) : socket_(-1), serverIp_(serverIp), serverPort_(serverPort) { } ~TcpClient() { } public: void init() { // 1. 创建socket SOCK_STREAM socket_ = socket(AF_INET, SOCK_STREAM, 0); if (socket_ < 0) { logMessage(FATAL, "client socket err"); exit(-1); } logMessage(DEBUG, "cliennt socket success!"); // 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽 // 2.1 先填充需要连接的远端主机的基本信息 输入进来的数据是 h -》n struct sockaddr_in RmtPort; bzero(&RmtPort, sizeof(RmtPort)); RmtPort.sin_family = AF_INET; //网络通信 inet_aton(serverIp_.c_str(), &RmtPort.sin_addr);// hton ip RmtPort.sin_port = htons(serverPort_);//hton port // 2.2 发起请求,connect 会自动帮我们进行bind! int ret = connect(socket_, (const struct sockaddr *)&RmtPort, sizeof(RmtPort)); if (ret != 0) { logMessage(FATAL, "client connect err"); exit(-1); } logMessage(DEBUG, "cliennt connect success!"); } #define READ_SIZE 1024 void recverServer() { cout << "client start recvfrom server message !" << endl; std::string message; while (!quit) { cout << "please echo your massege # "; getline(std::cin, message); if (strcasecmp(message.c_str(), "quit") == 0)//strcasecmp 忽略大小写匹配 quit = true; ssize_t s = write(socket_,message.c_str(),message.size());//把消息写给服务器 if(s>0)//写入成功接收服务器的消息 { message.resize(1024); ssize_t s = read(socket_,(char*)message.c_str(),READ_SIZE);//读写不是同一个缓冲区,不用担心自己写了自己读 if(s>0) { message[s]='\0'; } cout<<"Server return# "< Windows
额..........网上是有的,上面也已经提到了,我相信以大家的实力也不用我贴代码了吧,虽然我开始是想写的,但是我懒...或者说写这篇文章快把我整郁闷了(bug真的多),等之后再有想法了我再回来补上吧=。=。
最后的最后,创作不易,希望读者三连支持
赠人玫瑰,手有余香