源IP地址—目的地址:来自哪,到哪去
端口号:发往那个端口的数据应该由哪个进程来处理
一个端口只能被一个进程占用;一个进程可以多个端口占用。
每条数据有源端口号,目的端口号。
传输层协议选择:
有连接 VS 无连接:
有连接:双方必须都同意,才可以进行数据传输(打电话,必须等对方接通才能通话)
无连接:双方不一定都同意,也可以进行数据传输(发QQ,无论在不在线,你条件允许都能发过去)
可靠传输 VS 不可靠传输:
可靠传输:不一定是100%发送成功,毕竟网线断了不可能发送成功。是指发送成功,自己知道;发送失败,自己也知道。
不可靠传输:发送失败或成功,自己也不知道。
网络字节序:默认大端存储。
字节序:CPU在内存中对数据进行存取的顺序。
主机字节序:
大端字节序:高位存低地址
小端字节序:低位存低地址
注:主机字节序取决CPU架构:X86_小端 MIPS_大端
网络字节序 和 主机字节序 的转换:
#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntonl(uint32_t netlong);
uint16_t ntonl(uint16_t netlong);
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
socket 编程接口:(注socket 本质上是文件描述符)
- int socket(int domain, int type, int protocol);
创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
参数:
domain:地址域
type:套接字类型
protocol:0就好了
- int bind(int socket, const struct sockaddr *address, socklen_t address_len);
绑定端口号 (TCP/UDP, 服务器) ——把文件和端口关联在一起。(客户端也是能用的但是几乎不用)
确实是能用端口号关联,更准确地说是和socket 关联在一起(一个进程可以关联多个端口,一个端口关联一个进程)
注:什么叫服务端,客户端?(主动的一方就是客户端,被动的一方是服务端。二者可以随时转变的)
- int listen(int socket, int backlog);
开始监听socket (TCP, 服务器) —服务器来使用(手机开机信号良好)
参数:
socket:文件描述符
backlog:被动的socket,把socket 编程服务器 socket ,等待别人和连接通话
- int accept(int socket, struct sockaddr* address,socklen_t* address_len);
接收请求 (TCP, 服务器) —服务器来使用(别人打来电话,按下接听键)
参数:
socket:文件描述符
struct sockaddr* address:对端的 ip 地址,端口号。
返回值:是一个socket,这非常关键
- int connect(int sockfd, const struct sockaddr *addr ,socklen_t addrlen);
建立连接 (TCP, 客户端)—客户端(你给别人拨通电话)
参数:
sockfd:文件描述符
struct sockaddr* address:对端的 ip 地址,端口号。
这是队列图。
发送/接收
1、read/write也可以读写socket(TCP,不能针对UDP的socket)
2、recv/send(TCP,用法和read/write很相似,但是功能更丰富)
3、recvfrom/sendto(UDP)
代码示例:
SOCKET:
//封装的形式实现 #include <stdio.h> #include <cstdio> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <cstringt> #include <sys/socket.h> //socket相关 #include <netinet/in.h> #include <arpa/inet.h>//htons() #include <cstringt> //sendto() class TcpSocket { public: TcpSocket() :fd_(-1){ } bool Socket() { //和 UDP 不一样是第二个函数 SOCK_STREAM 意思面向字节流 fd_ =socket(AF_INET,SOCK_STREAM,0); if (fd_ < 0) { perror("socket"); return false; } return true; } //给服务器 bool Bind(const std::string& ip,uint16_t port) { sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(ip.c_str()); addr.sin_port = htons(port); int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr)); if (ret<0) { perror("bind"); return false; } return true; } //给服务器 //进入监听状态 bool Listen() { //int listen(int socket, int backlog); //int socket:文件描述符 //int backlog:队列的长度 int ret=int listen(fd_, 10); if (ret<0) { perror("listen"); return false; } return true; } //给服务器 bool Accept(TcpSocket* peer, std::string& ip = NULL, uint16_t port=NULL) { //int accept(int socket, struct sockaddr* address, socklen_t* address_len); //struct sockaddr* address:对端的 ip 地址,端口号。 //从连接队列中去一个连接到用户代码中,如果队列中没有连接,阻塞(默认行为) sockaddr_in peer_addr; socklen_t len = sizeof(peer_addr); //设置初始值 //返回值也是一个socket,类似内场销售 int client_socket = accept(fd_, (sockaddr*)&peer_addr, &len); if (client_socket < 0) { perror("accept"); return false; } peer->fd_ = client_socket; if (ip !=NULL) { *ip = inet_ntoa(peer_addr.sin_addr); } if (port != NULL) { *port = ntohs(peer_addr.sin_port); } return true; } //给客户端+服务器 int Recv(std::string& msg) { msg->clear(); char buf[1024 * 10] = { 0 }; ssize_t n = recv(fd_, buf, sizeof(buf)-1, 0); //返回值成功:返回读到的字节数;失败,返回-1,如果是对端关闭了socket,但会结果是 0 。 if (n<0) { perror("recv"); return -1; } else if (n==0) { return 0; } msg->assign(buf); //读到的数据赋值到msg中 return 1; } //给客户端+服务器 bool Send(const std::string& msg) { ssize_t n = send(fd_, msg.c_str(), msg.size(), 0); if (n < 0) { perror("send"); return false; } return true; } //给客户端 bool Connect(const std::string& ip, uint16_t port) { sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = inet_addr(ip.c_str()); int ret = connect(fd_, (sockaddr*)&addr, sizeof(addr)); if (ret < 0) { perror("connect"); return false; } return true; } bool Close() { if (fd_ != 1) { close(fd_); } return true; } private: int fd_; };
服务器:
//通用的TCP 服务器 #pragma once #include "TCPSOCKET.hpp" #include <functional> #include <cassert> typedef std::function<void(const std::string&, std::string*)> Handler; #define CHECK_RET(exp)if(!(exp)) {\ return false; \ } class TcpServer { public: TcpServer(); ~TcpServer(); bool Start(const std::string& ip, uint16_t port , Handler handler) { //1.创建socket CHECK_RET( listen_sock_.Socket()); /* 等价与: bool ret = listen_sock_.Socket(); if (! ret) { return false; }*/ //2.绑定端口号 CHECK_RET(listen_sock_.Bind(ip, port)); // listen_sock_ :负责拉客。把客户端的连接建立好,建立好之后,client_sock来处理。 //3.开始监听 CHECK_RET(listen_sock_.Listen()); printf("启动成功!"); //4.主循环 while (true) { //5.通过Accept 获取连接 TcpSocket client_sock; std::string ip; uint16_t port; bool ret = listen_sock_.Accept(&client_sock,&ip,&port); if (!ret) { continue; } //6.进行具体的沟通。 //因为要连接,所以要多次沟通,打电话彼此之间说很多,反反复复沟通。 while (true) { std::string req; int r =client_sock.Recv(&req); if (r<0) { continue; } if (r == 0 ) { //对端关闭 //对面挂电话了 client_sock.Close(); printf("[%s:%d]对端关闭连接!\n",ip.c_str(),port); break; } if (r>0) { //根据请求计算响应 std::string resp; handler(req, &resp); //响应写回客户端 client_sock.Send(resp); } } } } private: TcpSocket listen_sock_; };
客户端
#include "TCPSOCKET.hpp" class TcpClient { public: TcpClient() { sock_.Socket(); } ~TcpClient() { sock_.Close(); } bool Connect(const std::string& ip, uint16_t port) { return sock_.Connect(ip,port); } int Recv(std::string* msg) { return sock_.Recv(msg); } bool Send(const std::string& msg) { return sock_.Send(msg); } private: TcpSocket sock_; };
TCP只能英文翻译示例代码:
服务器:
#include "封装TCP服务端.hpp" #include <isunordered_map> int main() { TcpServer server; std::isunordered_map<std::string, std::string> dict; dict.insert(std::make_pair("heallo", "你好")); dict.insert(std::make_pair("world", "世界")); dict.insert(std::make_pair("bit", "比特")); server.Start("0.0.0.0", 9090, [](const std::string& req, std::string* resp) { auto it = dict.find(req); if (it == dict.end()) { *resp = "未找到"; } else { *resp = it->second; } }); system("pause"); return 0; }
客户端:
#include "封装的TCP客户端.hpp" int main(int argc,char* argv[]) { if (argc !=2) { printf("Usage ./dict_client [ip]\n"); return 1; } TcpClient clicent; bool ret = clicent.Connect(argv[1], 9090); if (!ret) { return 1; } while (true) { //从标准输入数据 printf("请输入要查询的单词:\n"); fflush(stdout); char req[1024] = { 0 }; scanf("%s", req); //把读到的数据给服务器 clicent.Send(req); std::string resp; //读取服务器响应的结果 clicent.Recv(&resp); //把响应的结果打印到标准输出上 printf("resp: %s\n", resp.c_str()); } system("pause"); return 0; }
问题:打开一个新的客户端,之后的就没用了,第二个listen_sock 不会被取到,之前的就取到了不会变了。核心问题在于Accept 只被调用了一次。第一次Accept之后进入一个循环,循环一直没结束,Accept没有被反复调用到,后过来的客户端都在队列中排队,一直得不到处理。就要想办法更快速得调用到Accept。