【计算机网络】模拟一个基于TCP协议的简单的阻塞式的网络聊天工具

本文主要是介绍【计算机网络】模拟一个基于TCP协议的简单的阻塞式的网络聊天工具,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1、socket API 常用函数

这些函数都在sys/socket.h中。

1.1 socket()
 #include <sys/types.h>          /* See NOTES */#include <sys/socket.h>int socket(int domain, int type, int protocol);

domain

       Name                Purpose                          Man pageAF_UNIX, AF_LOCAL   Local communication              unix(7)AF_INET             IPv4 Internet protocols          ip(7)AF_INET6            IPv6 Internet protocols          ipv6(7)AF_IPX              IPX - Novell protocolsAF_NETLINK          Kernel user interface device     netlink(7)AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)AF_AX25             Amateur radio AX.25 protocolAF_ATMPVC           Access to raw ATM PVCsAF_APPLETALK        Appletalk                        ddp(7)AF_PACKET           Low level packet interface       packet(7)

type

SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission mecha-nism may be supported.SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).SOCK_SEQPACKET  Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximumlength; a consumer is required to read an entire packet with each input system call.SOCK_RAW        Provides raw network protocol access.SOCK_RDM        Provides a reliable datagram layer that does not guarantee ordering.SOCK_PACKET     Obsolete and should not be used in new programs; see packet(7).

注意:

  • socket打开一个网络通讯端口,如果成功的话就像read一样返回一个文件描述符。
  • 应用程序可以向读写文件一样用read/write在网络上收发数据。
  • 如果socket调用出错则返回-1。
  • 对于IPv4,family参数指定为AF_INET。
  • 对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。
  • protocol参数的介绍从略,指定为0即可。
1.2 bind()
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数

When a socket is created with socket(2), it exists in a name space (address family) but has no address assigned to it.  bind()
assigns the address specified to by addr to the socket referred to by the file descriptor sockfd.  addrlen specifies the size,
in  bytes,  of  the  address  structure  pointed  to  by addr.  Traditionally, this operation is called “assigning a name to a socket”.It is normally necessary to assign a local address using bind() before a  SOCK_STREAM  socket  may  receive  connections  (see accept(2)).The  rules  used in name binding vary between address families.  Consult the manual entries in Section 7 for detailed information.  For AF_INET see ip(7), for AF_INET6 see ipv6(7), for AF_UNIX see unix(7), for AF_APPLETALK see  ddp(7),  for  AF_PACKET see packet(7), for AF_X25 see x25(7) and for AF_NETLINK see netlink(7).

注意:

  • 服务器程序所监听的⺴络地址和端⼝号通常是固定不变的,客户端程序得知服务器程序的地址和端⼝号后就可以向服务器发起连接; 服务器需要调⽤bind绑定⼀个固定的⺴络地址和端⼝号;
  • bind()成功返回0,失败返回-1。
  • bind()的作⽤是将参数sockfd和myaddr绑定在⼀起, 使sockfd这个⽤于网络通讯的⽂件描述符监听myaddr所描述的地址和端⼝号;
  • struct sockaddr *是⼀个通⽤指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,⽽它们的⻓度各不相同,所以需要第三个参数addrlen指定结构体的⻓度;
1.3 listen()
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>int listen(int sockfd, int backlog);

参数:

listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow.  If  a  connection  request  arrives  when  the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds.

注意:

  • listen()声明socket处于监听状态,并且做多允许有backlog个客户端处于连接等待状态。
  • listen()成功返回0,失败返回-1。
1.4 accept()
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

sockfd:socket文件描述符
addr:sockaddr类型结构体
addrlen:sockaddr类型结构体大小

注意:

  • 三次握⼿完成后, 服务器调⽤accept()接受连接;
  • 如果服务器调⽤accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • addr是⼀个传出参数,accept()返回时传出客户端的地址和端⼝号;
  • 如果给addr 参数传NULL,表⽰不关⼼客户端的地址;
  • addrlen参数是⼀个传⼊传出参数(value-result argument), 传⼊的是调⽤者提供的, 缓冲区addr的⻓度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际⻓度(有可能没有占满调⽤者提供的缓冲区);

2、服务器端

2.1 基本步骤:
  1. 创建socket
  2. 绑定端口号
  3. 把socket转换成被动模式(listen)
  4. 循环的进行accept
  5. 从accept返回的new_fd之中读取客户端的请求
  6. 根据读到的请求进行计算和处理
  7. 把处理后的结果返回给客户端
2.2 源码:
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<errno.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>#define _PORT_ 9999
#define _BACKLOG_ 10int main(){//创建socket文件描述符(TCP  客户端 + 服务器)int sock = socket(AF_INET,SOCK_STREAM,0);if(sock < 0){printf("create socket error,error : %d , error string : %s \n",errno,strerror(errno));}//sockaddr结构(地址类型 端口号 IP地址)struct sockaddr_in server_socket;struct sockaddr_in client_socket;//初始化server_socketbzero(&server_socket,sizeof(server_socket));server_socket.sin_family = AF_INET;//INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或"所有地址"、"任意地址"。server_socket.sin_addr.s_addr = htonl(INADDR_ANY);server_socket.sin_port = htons(_PORT_);//绑定端口号if(bind(sock,(struct sockaddr*)&server_socket,sizeof(struct sockaddr_in)) < 0){//绑定失败printf("bind error,errno: %d , error string: %s \n",errno,strerror(errno));close(sock);return 1;}//开始监听socket//_BACKLOG_   最多可监听10个if(listen(sock,_BACKLOG_) < 0){printf("lisen error,errno: %d , error string: %s \n",errno,strerror(errno));close(sock);return 2;}printf("bind and listen success! wait accept...\n");//循环接收客户端的请求while(1){socklen_t len = 0;int client_sock = accept(sock,(struct sockaddr *)&client_socket,&len);if(client_sock < 0){printf("accept error,client ip: %s , error: %d , error string: %s \n",inet_ntoa(client_socket.sin_addr),errno,strerror(errno));close(sock);return 3;}//从accept返回的client_sock中读取客户端的请求char buf_ip[INET_ADDRSTRLEN] = {0};//将客户端IP地址转换成字符串存入缓冲区buf_ip中inet_ntop(AF_INET,&client_socket.sin_addr,buf_ip,sizeof(buf_ip));printf("[connect] ip: %s ,port: %d\n",buf_ip,ntohs(client_socket.sin_port));while(1){//根据读取到的请求进行计算和处理//这里实现一个简单的回显服务器,对客户端请求不作任何处理,直接输出char buf[1024] = {0};read(client_sock,buf,sizeof(buf));printf("client :# %s\n",buf);printf("server :$ ");//把处理后的结果发送给客户端memset(buf,'\0',sizeof(buf));fgets(buf,sizeof(buf),stdin);buf[strlen(buf)-1] = '\0';write(client_sock,buf,strlen(buf)+1);printf("please wait...\n");}}close(sock);return 0;}

3、客户端

3.1 基本步骤:
  1. 创建socket
  2. 和服务器建立连接
  3. 给服务器发送数据
  4. 从服务器读取返回的结果
  5. 和服务器断开连接
3.2 源码:
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<string.h>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>#define SERVER_PORT 9999
#define SERVER_IP "192.168.11.128"int main(int argc,char *argv[]){if(argc != 2){printf("Usage:client IP\n");return 1;}char *str = argv[1];char buf[1024];memset(buf,'\0',sizeof(buf));struct sockaddr_in server_sock;int sock = socket(AF_INET,SOCK_STREAM,0);bzero(&server_sock,sizeof(server_sock));server_sock.sin_family = AF_INET;//字符串SERVER_IP转成in_addr server_sockinet_pton(AF_INET,SERVER_IP,&server_sock.sin_addr);server_sock.sin_port = htons(SERVER_PORT);int ret = connect(sock,(struct sockaddr *)&server_sock,sizeof(server_sock));if(ret<0){printf("connect failed ...,errno is : %d,errstring is: %s\n",errno,strerror(errno));return 1;}printf("connect success...\n");while(1){printf("client:#");fgets(buf,sizeof(buf),stdin);buf[strlen(buf)-1] = '\0';write(sock,buf,sizeof(buf));if(strncasecmp(buf,"quit",4) == 0){printf("quit!\n");break;}printf("please wait ...\n");read(sock,buf,sizeof(buf));printf("server :$ %s\n",buf);}close(sock);return 0;
}
3.3 注意:
  • 由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。
  • 客户端没有必要非要绑定端口号,否则如果在一台机器上启动多个客户端,就会出现端口号被占用,导致不能正确建立连接。
  • 服务器端如果不绑定端口号的话,内核会给服务器端自动分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇见麻烦。
  • 客户端需要调用connect()连接服务器。connect和bind的参数形式一样,区别在于bind的参数是自己的地址,connect的参数是对方的地址。
  • connect调用失败的几个可能的原因:服务器没有关闭防火墙,服务器没启动、地址错误、端口错误、类型错误等。

4、程序运行结果

服务器:
这里写图片描述

客户端:
这里写图片描述

查看监听状态:

这里写图片描述

这里写图片描述

5、缺陷

5.1 测试多个连接的情况

客户端1:

这里写图片描述

客户端2:

这里写图片描述

服务器:

这里写图片描述

这时我们发现第二个客户端并不能正确的和服务器建立连接。

5.2 原因分析:

是因为我们accecpt了⼀个请求之后, 就在⼀直while循环尝试read, 没有继续调⽤到accecpt, 导致不能接受新的请求。

我们当前的这个TCP, 只能处理⼀个连接。

改进方法:

想要服务器可以处理多个客户端的请求,可以依靠多线程/多进程/生产者消费者模型来实现。

这篇关于【计算机网络】模拟一个基于TCP协议的简单的阻塞式的网络聊天工具的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/615295

相关文章

基于Qt开发一个简单的OFD阅读器

《基于Qt开发一个简单的OFD阅读器》这篇文章主要为大家详细介绍了如何使用Qt框架开发一个功能强大且性能优异的OFD阅读器,文中的示例代码讲解详细,有需要的小伙伴可以参考一下... 目录摘要引言一、OFD文件格式解析二、文档结构解析三、页面渲染四、用户交互五、性能优化六、示例代码七、未来发展方向八、结论摘要

Java如何接收并解析HL7协议数据

《Java如何接收并解析HL7协议数据》文章主要介绍了HL7协议及其在医疗行业中的应用,详细描述了如何配置环境、接收和解析数据,以及与前端进行交互的实现方法,文章还分享了使用7Edit工具进行调试的经... 目录一、前言二、正文1、环境配置2、数据接收:HL7Monitor3、数据解析:HL7Busines

MyBatis框架实现一个简单的数据查询操作

《MyBatis框架实现一个简单的数据查询操作》本文介绍了MyBatis框架下进行数据查询操作的详细步骤,括创建实体类、编写SQL标签、配置Mapper、开启驼峰命名映射以及执行SQL语句等,感兴趣的... 基于在前面几章我们已经学习了对MyBATis进行环境配置,并利用SqlSessionFactory核

QT实现TCP客户端自动连接

《QT实现TCP客户端自动连接》这篇文章主要为大家详细介绍了QT中一个TCP客户端自动连接的测试模型,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录版本 1:没有取消按钮 测试效果测试代码版本 2:有取消按钮测试效果测试代码版本 1:没有取消按钮 测试效果缺陷:无法手动停

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu2289(简单二分)

虽说是简单二分,但是我还是wa死了  题意:已知圆台的体积,求高度 首先要知道圆台体积怎么求:设上下底的半径分别为r1,r2,高为h,V = PI*(r1*r1+r1*r2+r2*r2)*h/3 然后以h进行二分 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#includ

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

usaco 1.3 Prime Cryptarithm(简单哈希表暴搜剪枝)

思路: 1. 用一个 hash[ ] 数组存放输入的数字,令 hash[ tmp ]=1 。 2. 一个自定义函数 check( ) ,检查各位是否为输入的数字。 3. 暴搜。第一行数从 100到999,第二行数从 10到99。 4. 剪枝。 代码: /*ID: who jayLANG: C++TASK: crypt1*/#include<stdio.h>bool h

usaco 1.2 Transformations(模拟)

我的做法就是一个一个情况枚举出来 注意计算公式: ( 变换后的矩阵记为C) 顺时针旋转90°:C[i] [j]=A[n-j-1] [i] (旋转180°和270° 可以多转几个九十度来推) 对称:C[i] [n-j-1]=A[i] [j] 代码有点长 。。。 /*ID: who jayLANG: C++TASK: transform*/#include<