计算机网络|[计算机网络]套接字编程

●个人主页:你帅你先说.
●欢迎点赞关注收藏
●既选择了远方,便只顾风雨兼程。
●欢迎大家有问题随时私信我!
●版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。

为您导航
  • 1.IP地址、端口号、TCP/UDP协议
    • 1.1源IP地址和目的IP地址
    • 1.2端口号
    • 1.3TCP协议
    • 1.4UDP协议
    • 1.5网络字节序
  • 2.socket编程接口
    • 2.1sockaddr结构
    • 2.2常用接口
      • 2.2.1创建套接字
      • 2.2.2绑定套接字文件
      • 2.2.3接收数据
      • 2.2.4返还数据
    • 2.3UDP实现客户端和服务端
    • 2.3TCP实现客户端和服务端
  • 3.TCP协议通讯流程
    • 3.1五个调用
    • 3.2三次握手
    • 3.3数据传输
    • 3.4四次挥手

1.IP地址、端口号、TCP/UDP协议 1.1源IP地址和目的IP地址
主机A和主机B进行通信,主机A就是源IP地址,主机B就是目的IP地址。
1.2端口号
端口号(port)是传输层协议的内容。
  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
    * IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。这个组合还有另一个名字:socket。
  • 一个端口号只能被一个进程占用。
IP地址仅仅是解决了两台物理机器之间的互相通信,那端口号是用来做什么的?
在我们的应用层之上会存在很多进程,这台主机的进程可能会与另一台主机的进程进行通信,有那么多进程为了保证通信的准确信,就有了端口号,端口号唯一的标识一台机器上的一个进程。
1.3TCP协议 TCP(Transmission Control Protocol 传输控制协议)
  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流
1.4UDP协议 UDP(User Datagram Protocol 用户数据报协议)
  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报
1.5网络字节序 我们已经知道,内存中的多字节数据相对于内存地址有大端小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分。我们这样定义网络数据流的地址。
  1. 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  2. 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  3. 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  4. TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  5. 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  6. 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include //h就是host(主机),n就是network(网络) uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);

2.socket编程接口 2.1sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同。
计算机网络|[计算机网络]套接字编程
文章图片

2.2常用接口 2.2.1创建套接字
#include #include int socket(int domain, int type, int protocol); domain:套接字种类,例如AF_INET,AF_UNIX type:套接字类型,例如TCP的SOCK_STREAM,UDP的SOCK_DGRAM protocol:使用的协议类型,一般设置为0 返回值:成功返回一个文件描述符,失败了返回-1

domain:
计算机网络|[计算机网络]套接字编程
文章图片

type:
计算机网络|[计算机网络]套接字编程
文章图片

2.2.2绑定套接字文件
int bind(int sockfd,const struct sockaddr * addr,socklen_t addrlen); sockfd:文件描述符 addr:一般传入一个sockaddr_in的结构体,包含套接字种类、端口、IP地址。因为类型不匹配,一般需要进行强转。 addrlen:addr的大小 返回值:成功返回0,失败返回-1。

struct sockaddr_in { _SOCKADDR_COMMON(sin_); short int sin_family; //套接字种类 in_port sin_port; //端口号 struct in_addr sin_addr; //IP地址 //...... };

2.2.3接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); sockfd:文件描述符 buf:缓冲区 len:缓冲区大小 flags:读取方式,默认为0 src_addr:和你服务器通信的客户端的套接字信息 addrlen:src_addr大小 返回值:成功返回发送的字节数,失败返回-1。

2.2.4返还数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen); sockfd:文件描述符 buf:缓冲区 len:缓冲区大小 flags:读取方式,默认为0 dest_addr:和你服务器通信的客户端的套接字信息 addrlen:src_addr大小 返回值:成功返回发送的字节数,失败返回-1。

2.3UDP实现客户端和服务端 有了上面的函数接口介绍,我们来实现一个简易的基于udp协议的客户端和服务端
makefile
.PHONY:all all:udp_server udp_clientudp_server:udp_server.cpp g++ -o $@ $^ -std=c++11udp_client:udp_client.cpp g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f udp_client udp_server

udp_server.cpp
#include #include #include #include #include #include #include #include #define NUM 1024// const uint16_t port = 8080; std::string Usage(std::string proc) { std::cout <<"Usage: "<< proc << " port" << std::endl; }// ./udp_server port int main(int argc, char *argv[]) { if(argc != 2){ Usage(argv[0]); return -1; }uint16_t port = atoi(argv[1]); // 1. 创建套接字,打开网络文件 int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { std::cerr << "socket create error: " << errno << std::endl; return 1; }// 2. 给该服务器绑定端口和ip struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(port); //此处的端口号,是我们计算机上的变量,是主机序列 // 需要将人识别的点分十进制,字符串风格IP地址,转化成为4字节整数IP,也要考虑大小端。 // in_addr_t inet_addr(const char *cp); 能完成上面ab两个工作. // 云服务器,不允许用户直接bind公网IP,另外, 实际正常编写的时候,我们也不会指明IP // local.sin_addr.s_addr = inet_addr("22.190.23.183"); //点分十进制,字符串风格[0-255].[0-255].[0-255].[0-255] // INADDR_ANY: 如果你bind的是确定的IP(主机), 意味着只有发到该IP主机上面的数据 // 才会交给你的网络进程, 但是,一般服务器可能有多张网卡,配置多个IP,我们需要的不是 // 某个IP上面的数据,我们需要的是,所有发送到该主机,发送到该端口的数据! //INADDR_ANY的作用也就是说不关心你是哪个IP,只要你访问的是我这个端口,都要把数据接收。 //INADDR_ANY相当于绑定0.0.0.0地址,这个地址表示绑定任意一个地址或者说所有地址。 local.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0) { std::cerr << "bind error : " << errno << std::endl; return 2; }// 3. 提供服务 bool quit = false; char buffer[NUM]; while (!quit) { struct sockaddr_in peer; socklen_t len = sizeof(peer); //通过发送字符串进行通信 ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); if (cnt > 0) { buffer[cnt] = 0; //popen(const char* commond,const char* type); 可以执行字符串里的命令 FILE *fp = popen(buffer, "r"); std::string echo_hello; char line[1024] = {0}; while(fgets(line, sizeof(line), fp) != NULL) { echo_hello += line; } pclose(fp); std::cout << "client# " << buffer << std::endl; sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr *)&peer, len); } }return 0; }

udp_client.cpp
#include #include #include #include #include #include #include #include void Usage(std::string proc) { std::cout << "Usage: \n\t" << proc << " server_ip server_port" << std::endl; }// ./udp_client server_ip server_portint main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); return 0; }// 1. 创建套接字,打开网络文件 int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { std::cerr << "socket error : " << errno << std::endl; return 1; }/*客户端需要显式的bind吗? 首先,客户端必须也要有ip和port,但是,客户端不需要显式的bind!一旦显示bind,就必须明确,client要和哪一个port关联 client指明的端口号,在client端一定会有吗??有可能被占用,被占用导致client无法使用 server要的是port必须明确,而且不变,但client只要有就行!一般是由OS自动给你bind() 就是client正常发送数据的时候,OS会自动给你bind,采用的是随机端口的方式!*/// 发送对象 struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(atoi(argv[2])); server.sin_addr.s_addr = inet_addr(argv[1]); // 使用服务 while (1) { std::cout << "MyShell $ "; char line[1024]; fgets(line, sizeof(line), stdin); sendto(sock, line, strlen(line), 0, (struct sockaddr*)&server, sizeof(server)); //此处tmp就是一个”占位符“ struct sockaddr_in tmp; socklen_t len = sizeof(tmp); char buffer[1024]; ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&tmp, &len); if(cnt > 0) { //在网络通信中,只有报文大小,或者是字节流中字节的个数,没有C/C++字符串这样的概念。 buffer[cnt] = 0; std::cout << buffer << std::endl; } }return 0; }

运行结果
计算机网络|[计算机网络]套接字编程
文章图片

127.0.0.1是本地ip,每个人都一样。
2.3TCP实现客户端和服务端 这里需要用到前面我们所讲的线程池代码,下面会给大家贴出来。
makefile
.PHONY:all all:tcp_client tcp_servertcp_client:tcp_client.cpp g++ -o $@ $^ -std=c++11tcp_server:tcp_server.cpp g++ -o $@ $^ -std=c++11 -lpthread.PHONY: clean: rm -f tcp_client tcp_server

thread_pool.hpp
#pragma once#include #include #include #include #include namespace ns_threadpool { const int g_num = 5; template class ThreadPool { private: int num_; std::queue task_queue_; pthread_mutex_t mtx_; pthread_cond_t cond_; static ThreadPool *ins; private: // 构造函数必须得实现,但是必须要私有化 ThreadPool(int num = g_num) : num_(num) { pthread_mutex_init(&mtx_, nullptr); pthread_cond_init(&cond_, nullptr); }//"=delete"的主要用途是阻止类的拷贝赋值 ThreadPool(const ThreadPool &tp) = delete; //C++11表明这个函数是被删除的函数,虽然声明了它,但不能使用它 ThreadPool &operator=(ThreadPool &tp) = delete; public: static ThreadPool *GetInstance() { static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 当前单例对象还没有被创建 if (ins == nullptr) //双判定,减少锁的争用,提高获取单例的效率! { pthread_mutex_lock(&lock); if (ins == nullptr) { ins = new ThreadPool(); ins->InitThreadPool(); std::cout << "首次加载对象" << std::endl; } pthread_mutex_unlock(&lock); }return ins; }void Lock() { pthread_mutex_lock(&mtx_); } void Unlock() { pthread_mutex_unlock(&mtx_); } void Wait() { pthread_cond_wait(&cond_, &mtx_); } void Wakeup() { pthread_cond_signal(&cond_); } bool IsEmpey() { return task_queue_.empty(); }public: // 在类中要让线程执行类内成员方法,是不可行的! // 必须让线程执行静态方法 static void *Rountine(void *args) { pthread_detach(pthread_self()); ThreadPool *tp = (ThreadPool *)args; while (true) { tp->Lock(); while (tp->IsEmpey()) { tp->Wait(); } T t; tp->PopTask(&t); tp->Unlock(); t(); } } void InitThreadPool() { pthread_t tid; for (int i = 0; i < num_; i++) { pthread_create(&tid, nullptr, Rountine, (void *)this); } } void PushTask(const T &in) { Lock(); task_queue_.push(in); Unlock(); Wakeup(); } void PopTask(T *out) { *out = task_queue_.front(); task_queue_.pop(); } ~ThreadPool() { pthread_mutex_destroy(&mtx_); pthread_cond_destroy(&cond_); } }; template ThreadPool *ThreadPool::ins = nullptr; }

Task.hpp
#pragma once#include #include #include namespace ns_task { class Task { private: int sock; public: Task() : sock(-1) {} Task(int _sock) : sock(_sock) { } int Run() { //提供服务,我们是一个死循环 // while (true) // { char buffer[1024]; memset(buffer, 0, sizeof(buffer)); ssize_t s = read(sock, buffer, sizeof(buffer) - 1); if (s > 0) { buffer[s] = 0; //将获取的内容当成字符串 std::cout << "client# " << buffer << std::endl; //拉取逻辑 std::string echo_string = ">>>server<<<, "; echo_string += buffer; write(sock, echo_string.c_str(), echo_string.size()); } else if (s == 0) { std::cout << "client quit ..." << std::endl; // break; } else { std::cerr << "read error" << std::endl; // break; } // }close(sock); } ~Task() {} }; }

tcp_client.cpp
#include #include #include #include #include #include #include void Usage(std::string proc) { std::cout << "Usage: " << proc << " server_ip server_port" << std::endl; } // ./udp_client server_ip server_port int main(int argc, char *argv[]) { if(argc != 3) { Usage(argv[0]); return 1; } std::string svr_ip = argv[1]; uint16_t svr_port = (uint16_t)atoi(argv[2]); //1. 创建socket int sock = socket(AF_INET, SOCK_STREAM, 0); if(sock < 0) { std::cerr << "socket error!" << std::endl; return 2; } //2. bind struct sockaddr_in server; bzero(&server, sizeof(server)); //和前面使用memset一样,全部初始化为0。 server.sin_family = AF_INET; //该函数做两件事情 //1. 将点分十进制的字符串风格的IP,转化成为4字节IP //2. 将4字节由主机序列转化成为网络序列 server.sin_addr.s_addr = inet_addr(svr_ip.c_str()); //将网络主机地址转为网络字节序二进制值 server.sin_port = htons(svr_port); // 主机转网络字节序//2. 发起链接 if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) { std::cout << "connect server failed !" << std::endl; return 3; }std::cout << "connect success!" << std::endl; // 进行正常的业务请求了 while(true) { std::cout << "Please Enter# "; char buffer[1024]; fgets(buffer, sizeof(buffer)-1, stdin); write(sock, buffer, strlen(buffer)); ssize_t s = read(sock, buffer, sizeof(buffer)-1); if(s > 0) { buffer[s] = 0; std::cout << "server echo# " << buffer << std::endl; } }return 0; }

tcp_server.cpp
#include #include #include #include #include #include #include #include #include #include #include #include #include "Task.hpp" #include "thread_pool.hpp"using namespace ns_threadpool; using namespace ns_task; void Usage(std::string proc) { std::cout << "Usage: " << proc << " port" << std::endl; } // ./tcp_server 8081 int main(int argc, char *argv[]) { if(argc != 2) { Usage(argv[0]); return 1; } // 创建套接字 int listen_sock = socket(AF_INET, SOCK_STREAM, 0); //listen_sock是一个监听套接字 if(listen_sock < 0) { std::cerr <<"socket error: " << errno << std::endl; return 2; }// bind 套接字和文件信息进行关联 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(atoi(argv[1])); local.sin_addr.s_addr = INADDR_ANY; if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0) { std::cerr << "bind error: " << errno << std::endl; return 3; } //3. 因为tcp是面向连接的,在通信前,需要建连接,然后才能通信。 //一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务) //我们当前写的是一个server, 周而复始的不间断的等待客户到来 //我们要不断的给用户提供一个建立连接的功能 //设置套接字是Listen状态, 本质是允许用户连接 const int back_log = 5; // listen 设置socket文件的状态,允许用户来连接我 if(listen(listen_sock, back_log) < 0) { std::cerr << "listen error" << std::endl; return 4; }for( ; ; ) { struct sockaddr_in peer; socklen_t len = sizeof(peer); // accept 获取新连接到应用层,新连接在OS层面,本质上其实就是一个描述连接的结构体。 int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len); if(new_sock < 0) { continue; } uint16_t cli_port = ntohs(peer.sin_port); //网络字节序转为主机字节序 std::string cli_ip = inet_ntoa(peer.sin_addr); //将网络字节序转为点分十进制std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port <<"]# " << new_sock << std::endl; //创建线程、进程无上限,当客户链接来了,我们才给客户创建进程/线程 //1. 构建一个任务 Task t(new_sock); //2. 将任务push到后端的线程池即可 ThreadPool::GetInstance()->PushTask(t); }return 0; }

3.TCP协议通讯流程 3.1五个调用
调用socket, 创建文件描述符
调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败。
调用connect, 向服务器发起连接请求
调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备。
调用accecpt, 并阻塞, 等待客户端连接过来
3.2三次握手
connect会发出SYN段并阻塞等待服务器应答。(第一次)
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"。 (第二次)。
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段。 (第三次)
3.3数据传输
建立连接后,TCP协议提供全双工的通信服务。 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 。相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据。
服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待。
这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答。
服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求。
客户端收到后从read()返回, 发送下一条请求,如此循环下去。
3.4四次挥手
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段。(第一次)
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 。(第二次)
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN。(第三次)
客户端收到FIN, 再返回一个ACK给服务器。(第四次)
【计算机网络|[计算机网络]套接字编程】喜欢这篇文章的可以给个一键三连点赞关注收藏

    推荐阅读