IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 【网络编程】TCP socket 编程实验 -> 正文阅读

[网络协议]【网络编程】TCP socket 编程实验

?总述

????????这是一个简单的 socket 编程实验,在主函数内创建 server 和 client 两个线程,实现单台机器上的 socket 通信,作为后面 IO 多路复用实验和 TCP 连接状态实验的基础。

? ? ? ? 对于应用层来说,TCP 通信都是调用 socket 接口,底层的协议栈被接口屏蔽,具体的网络交互由操作系统内核来管理,socket 接口的调用流程也比较简单。对于一个 demo 性质的编程实验,代码量很小,需要注意的是 socket 接口的行为特点。但如果是作为工程化的代码,尤其是服务端,则要考虑多客户端接入、并发性能、网络断连、粘包这些问题,在很大程度上增加代码量和实现复杂度。

主体流程结构

实验结果

终端打印

报文交互

????????用 tcpdump -s 0 -w socketTest.pcap -i lo host 127.0.0.1 and port 5197 -v 命令抓包,用 wireshark 查看报文(server 主动断开连接,client 在发送 FIN 的同时,发送对 server FIN 的 ACK,四次挥手合并为三次):

?????????在封包详细信息部分,展开 Transmission Control Protocol,可以看到?TCP 头报文头的详细内容:

注意点(完整代码在最后)

  • 客户端持续请求连接

????????客户端可能在服务端开始监听之前,向服务端请求连接:

????????所以在客户端的 connet 接口外加了循环,如果请求连接失败则继续尝试:

  • listen 和 accpet

????????listen 是非阻塞的,调用 listen 接口只是告诉内核监听指定的端口。在建立连接的实现机制上,主要涉及两个队列 —— 未完成连接的队列 和 已完成连接的队列。当收到第一次握手的消息时,内核在未完成连接的队列中创建一个条目,在三次握手完成后,把条目转到已完成连接队列的尾部。当应用层调用 accept 时,从已完成连接队列的头部取出一个条目,返回给应用层。如果已完成连接的队列为空,则阻塞应用层接口。真正进行监听并和 client 三次握手建立连接的并不是 listen 或者 accept。反映在应用层代码的实现上,只需要调用一次 listen 接口即可,不需要持续等待 listen 接口返回请求连接的消息(listen 也不会返回)。

????????这也是为什么在实验结果里会出现 client 先打印连接成功, server 后打印接受连接(在 server 应用层调用的 accpet 返回之前,连接就已经建立好了,accept 是“吃现成的”):

  • 监听句柄和连接句柄

? ? ? ? 服务端调用 accept 接口会返回连接句柄,一个新的不同于监听句柄的句柄。实际通信用的都是连接句柄。

  • 避免进程过早退出

    ?在主函数中,创建服务端线程和客户端线程后,需调用 pthread_join 避免主函数过早退出。

?????????调用 pthread_join 的效果是阻塞主线程,直到等待的线程执行完毕,回收线程资源并获取线程的执行结果(线程入口函数返回值)。如果不调用 pthread_join,main 函数在调用 pthread_create 创建两个子线程后,直接执行 return,进程结束,导致所有子线程也结束,但是创建的两个子线程可能还没有执行完毕。

?? ? ? ? 关于线程可结合、可分离属性:

????????只有可结合的线程才能被 join,也应该要被 join。?线程默认是可结合的。如果可结合线程运行结束,但是没有被 join,线程的资源不会被释放。

????????相反的,一个可分离的线程不能被其他线程回收或杀死,在线程结束时由系统自动释放资源。可以设置 pthread_create 第二个参数 pthread_attr_t 结构中的 detachstate 属性,以可分离方式启动线程 或者 在线程启动后,调用?pthread_detach(thread_id) 设置线程为可分离。

? ? ? ? PS:在把创建的子线程属性设置为可分离的情况下,如果主函数先于子线程结束,子线程照样是立即结束。

完整代码实现

头文件

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>    // 用到了 pthread 库,编译要加选项 -lpthread

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

宏定义

#define LOCAL_IP_ADDR       "127.0.0.1"
#define SERVER_LISTEN_PORT  5197
#define NET_MSG_BUF_LEN     128
#define CLINET_SEND_MSG     "Hello Server~"
#define SERVER_SEND_MSG     "Hello Client~"

?服务端线程入口函数

void* socketServer(void* param){
    int iRes = 0;
    int iLsnFd, iConnFd;
    int iNetMsgLen = 0;
    socklen_t iSockAddrLen = 0;
    char szNetMsg[NET_MSG_BUF_LEN] = {0};
    struct sockaddr_in stLsnAddr;
    struct sockaddr_in stCliAddr;

    // 1 参指定协议族,AF_INET 对应 IPv4
    // 2 参指定套接字类型,SOCK_STREAM 对应 面向连接的流式套接字
    // 3 参指定协议类型,0 对应 TCP 协议
    iLsnFd = socket(AF_INET, SOCK_STREAM, 0);                       
    if (-1 == iLsnFd) {
        printf("Server failed to create socket, err[%s]\n", 
               strerror(errno));
        return NULL;
    }

    // 填写监听地址,设置 s_addr = INADDR_ANY 表示监听所有网卡上对应的端口
    stLsnAddr.sin_family = AF_INET;
    stLsnAddr.sin_port = htons(SERVER_LISTEN_PORT);
    stLsnAddr.sin_addr.s_addr = INADDR_ANY;
    // 1 参传入 socket 句柄,2 参传入监听地址,3 参传入监听地址结构体的大小
    iRes = bind(iLsnFd, (struct sockaddr*)&stLsnAddr, sizeof(stLsnAddr));   
    if (-1 == iRes) {
        printf("Server failed to bind port[%u], err[%s]\n", 
               SERVER_LISTEN_PORT, strerror(errno));
        close(iLsnFd);
        return NULL;
    } else {
        printf("Server succeeded to bind port[%u], start listen.\n",
               SERVER_LISTEN_PORT);
    }

    // 1 参传入监听句柄,2 参设置已完成连接队列(已完成三次握手,未 accept 的连接)的长度
    iRes = listen(iLsnFd, 16);
    if (-1 == iRes) {
        printf("Server failed to listen port[%u], err[%s]\n", 
               SERVER_LISTEN_PORT, strerror(errno));
        close(iLsnFd);
        return NULL;
    }

    iSockAddrLen = sizeof(stCliAddr);
    // 1 参传入监听句柄,2 传入地址结构体指针接收客户端地址,3 参传入地址结构体大小
    iConnFd = accept(iLsnFd, (struct sockaddr*)&stCliAddr, &iSockAddrLen);
    if (-1 == iConnFd) {
        printf("Server failed to accept connect request, err[%s]\n", 
               strerror(errno));
        close(iLsnFd);
        return NULL;
    } else {
        printf("Server accept connect request from[%s:%u]\n", 
			   inet_ntoa(stCliAddr.sin_addr), ntohs(stCliAddr.sin_port));
    }
	
    // 1 参传已连接套接字描述符,2 参传缓冲区指针,3 参传缓冲区大小,4 参指定行为,默认为 0
    iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
	if (iNetMsgLen < 0) {
		printf("Server failed to read from network, err[%s]\n", strerror(errno));
		close(iConnFd);
		close(iLsnFd);
		return NULL;
	} else {
		printf("Server recv msg[%s]\n", szNetMsg);
	}

    // 1 参传已连接套接字的描述符,2 参传指向消息数据的指针
    // 3 参传消息长度,4 参指定行为,默认为 0
    iNetMsgLen = send(iConnFd, SERVER_SEND_MSG, strlen(SERVER_SEND_MSG), 0);
    if (iNetMsgLen < 0) {
        printf("Server failed to reply client, err[%s]\n", strerror(errno));
    }

    close(iConnFd);
    close(iLsnFd);
    return NULL;
}

客户端线程入口函数

void* socketClient(void* param){
    int iRes = 0;
    int iConnFd;
    int iNetMsgLen = 0;
    char szNetMsg[NET_MSG_BUF_LEN] = {0};
    struct sockaddr_in stServAddr;

    iConnFd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == iConnFd) {
        printf("Client failed to create socket, err[%s]\n", strerror(errno));

        return NULL;
    }

    // 填充目标地址结构体,指定协议族、目标端口、目标主机 IP 地址
    stServAddr.sin_family = AF_INET;
    stServAddr.sin_port = htons(SERVER_LISTEN_PORT);
    stServAddr.sin_addr.s_addr = inet_addr(LOCAL_IP_ADDR);
    // 1 参传套接字句柄,2 参传准备连接的目标地址结构体指针,3 参传地址结构体大小
	while (1)
	{
		iRes = connect(iConnFd, (struct sockaddr *)&stServAddr, sizeof(stServAddr));
		if (0 != iRes) {
			printf("Client failed to connect to[%s:%u], err[%s]\n", 
				   LOCAL_IP_ADDR, SERVER_LISTEN_PORT, strerror(errno));
			sleep(60);
			continue;
		}
		else {
			printf("Client succeeded to connect to[%s:%u]\n", 
				   LOCAL_IP_ADDR, SERVER_LISTEN_PORT);
			break;
		}
	}

    iNetMsgLen = send(iConnFd, CLINET_SEND_MSG, strlen(CLINET_SEND_MSG), 0);
    if (iNetMsgLen < 0) {
        printf("Client failed to send msg to server, err[%s]\n", strerror(errno));
        close(iConnFd);
        return NULL;
    }

    iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
	if (iNetMsgLen < 0) {
		printf("Client failed to read from network, err[%s]\n", strerror(errno));
		close(iConnFd);
		return NULL;
	}
	else {
		printf("Client recv reply[%s]\n", szNetMsg);
	}
    
    close(iConnFd);
    return NULL;
}

主函数?

int main(){

    // 线程 ID,实质是 unsigned long 类型整数
    pthread_t thdServer = 1;
    pthread_t thdClient = 2;
    
    // 1 参传线程 ID,2 参传线程属性,3 参指定线程入口函数,4 参指定传给入口函数的参数
    pthread_create(&thdServer, NULL, socketServer, NULL);    
    pthread_create(&thdClient, NULL, socketClient, NULL);

    // 1 参传入线程 ID,2 参用于接收线程入口函数的返回值,不需要返回值则置 NULL
    pthread_join(thdServer, NULL);    
    pthread_join(thdClient, NULL);
    return 0;
}
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-09-05 11:23:21  更:2021-09-05 11:25:21 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年9日历 -2024/9/21 16:41:32-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码