基于TCP协议的网络程序

简介:

下图是基于TCP协议的客户端/服务器程序的一般流程:

图1.1 TCP协议通讯流程

wKioL1en6e6A3HjZAAFHuY9TmLk995.png

建立链接的过程:

图1.2  建立连接的过程

wKioL1en6hqDOmzlAACZ3XO_LCQ559.png

服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用 socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从 connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

数据传输的过程:

建立连接后,TCP 协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从 accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务 器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结 果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。


关闭链接的过程:

图1.3 关闭连接的过程

wKioL1en6mHxTIkTAACahabf0hk385.png 

如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了 连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用 shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

在学习socket API时要注意应用程序和TCP协议层是如何交互的: *应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 *应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段


先看一下需要用到的函数

1
2
3
4
5
6
7
8
9
10
11
NAME
        socket - create an endpoint  for  communication
 
SYNOPSIS
        #include <sys/types.h>           /* See NOTES */
        #include <sys/socket.h>
 
        int  socket( int  domain,  int  type,  int  protocol);
 
DESCRIPTION
        socket() creates an endpoint  for  communication and returns a descriptor.

socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用 read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协 议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的 传输协议。protocol参数的介绍从略,指定为0即可。

1
2
3
4
5
6
7
8
9
NAME
        bind - bind a name to a socket
 
SYNOPSIS
        #include <sys/types.h>           /* See NOTES */
        #include <sys/socket.h>
 
        int  bind( int  sockfd,  const  struct  sockaddr *addr,
                 socklen_t addrlen);

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。

bind() 的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。前面讲 过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定 结构体的长度。我们的程序中对myaddr参数是这样初始化的:

1
2
3
4
bzero(&servaddr,  sizeof (servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡, 每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为 SERV_PORT,我们定义为8000。

1
2
3
4
5
6
7
8
NAME
        listen - listen  for  connections on a socket
 
SYNOPSIS
        #include <sys/types.h>           /* See NOTES */
        #include <sys/socket.h>
 
        int  listen( int  sockfd,  int  backlog);

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务 器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于 连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

1
2
3
4
5
SYNOPSIS
        #include <sys/types.h>           /* See NOTES */
        #include <sys/socket.h>
 
        int  accept( int  sockfd,  struct  sockaddr *addr, socklen_t *addrlen);

三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。 cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者 提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。


服务器使这样子的:

1
2
3
4
5
6
7
8
while  (1) {
     cliaddr_len =  sizeof (cliaddr);
     connfd = accept(listenfd, 
             ( struct  sockaddr *)&cliaddr, &cliaddr_len);
     n = read(connfd, buf, MAXLINE);
     ......
     close(connfd);
}

整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。 accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过 这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。 accept()成功返回一个文件描述符,出错返回-1。


TCP网络程序:

server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。

/*server.c*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
 
#define MAXLINE 80
#define SERV_PORT 8000
 
int  main( void )
{
     struct  sockaddr_in servaddr, cliaddr; //定义套接字地址
     socklen_t cliaddr_len;
     int  listenfd, connfd;
     char  buf[MAXLINE];
     char  str[INET_ADDRSTRLEN];
     int  i, n;
 
     listenfd = socket(AF_INET, SOCK_STREAM, 0);
 
     bzero(&servaddr,  sizeof (servaddr));  //结构体初始化
     servaddr.sin_family = AF_INET;
     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY 宏
     servaddr.sin_port = htons(SERV_PORT);  //端口号
     
     bind(listenfd, ( struct  sockaddr *)&servaddr,  sizeof (servaddr));
 
     listen(listenfd, 20);
 
     printf ( "Accepting connections ...\n" );
     while  (1) {
         cliaddr_len =  sizeof (cliaddr);
         connfd = accept(listenfd, 
                 ( struct  sockaddr *)&cliaddr, &cliaddr_len);
       //accept()返回时传出客户端的地址和端口号
       
         n = read(connfd, buf, MAXLINE);
         printf ( "received from %s at PORT %d\n" ,
                inet_ntop(AF_INET, &cliaddr.sin_addr, str,  sizeof (str)),
                ntohs(cliaddr.sin_port));
     
         for  (i = 0; i < n; i++)
             buf[i] =  toupper (buf[i]); //用来将字符c转换为大写英文字母
         write(connfd, buf, n);
         close(connfd);
     }
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要 调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器 时端口号都不一样,客户端要连接服务器就会遇到麻烦。

1
2
3
4
5
6
7
8
9
NAME
        connect - initiate a connection on a socket
 
SYNOPSIS
        #include <sys/types.h>           /* See NOTES */
        #include <sys/socket.h>
 
        int  connect( int  sockfd,  const  struct  sockaddr *addr,
                    socklen_t addrlen);

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。


client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印。

/*client.c*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
 
#define MAXLINE 80
#define SERV_PORT 8000
 
int  main( int  argc,  char  *argv[])
{
     struct  sockaddr_in servaddr;
     char  buf[MAXLINE];
     int  sockfd, n;
     char  *str;
     
     if  (argc != 2) {
         fputs ( "usage: ./client message\n" , stderr);
         exit (1);
     }
     str = argv[1];
     
     sockfd = socket(AF_INET, SOCK_STREAM, 0);
 
     bzero(&servaddr,  sizeof (servaddr));
     servaddr.sin_family = AF_INET;
     inet_pton(AF_INET,  "127.0.0.1" , &servaddr.sin_addr);
     servaddr.sin_port = htons(SERV_PORT);
     
     connect(sockfd, ( struct  sockaddr *)&servaddr,  sizeof (servaddr));
 
     write(sockfd, str,  strlen (str));
 
     n = read(sockfd, buf, MAXLINE);
     printf ( "Response from server:\n" );
     write(STDOUT_FILENO, buf, n);
 
     close(sockfd);
     return  0;
}

打开两个终端,依次运行./server和./client tcp

wKioL1en7UmQteD1AACgiKOMgVY980.png



本文转自 七十七快 51CTO博客,原文链接:http://blog.51cto.com/10324228/1835531


相关文章
|
8天前
|
人工智能 自然语言处理 决策智能
智能体竟能自行组建通信网络,还能自创协议提升通信效率
《一种适用于大型语言模型网络的可扩展通信协议》提出创新协议Agora,解决多智能体系统中的“通信三难困境”,即异构性、通用性和成本问题。Agora通过标准协议、结构化数据和自然语言三种通信格式,实现高效协作,支持复杂任务自动化。演示场景显示其在预订服务和天气预报等应用中的优越性能。论文地址:https://arxiv.org/pdf/2410.11905。
24 6
|
9天前
|
网络协议 测试技术 Linux
Golang 实现轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库
gev 是一个基于 epoll 和 kqueue 实现的高性能事件循环库,适用于 Linux 和 macOS(Windows 暂不支持)。它支持多核多线程、动态扩容的 Ring Buffer 读写缓冲区、异步读写和 SO_REUSEPORT 端口重用。gev 使用少量 goroutine,监听连接并处理读写事件。性能测试显示其在不同配置下表现优异。安装命令:`go get -u github.com/Allenxuxu/gev`。
|
3月前
|
负载均衡 网络协议 算法
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
这网络层就像搭积木一样,上层协议都是基于下层协议搭出来的。不管是ping(用了ICMP协议)还是tcp本质上都是基于网络层IP协议的数据包,而到了物理层,都是二进制01串,都走网卡发出去了。 如果网络环境没发生变化,目的地又一样,那按道理说他们走的网络路径应该是一样的,什么情况下会不同呢? 我们就从路由这个话题聊起吧。
83 4
不为人知的网络编程(十九):能Ping通,TCP就一定能连接和通信吗?
|
3月前
|
前端开发 网络协议 安全
【网络原理】——HTTP协议、fiddler抓包
HTTP超文本传输,HTML,fiddler抓包,URL,urlencode,HTTP首行方法,GET方法,POST方法
|
3月前
|
网络协议
TCP报文格式全解析:网络小白变高手的必读指南
本文深入解析TCP报文格式,涵盖源端口、目的端口、序号、确认序号、首部长度、标志字段、窗口大小、检验和、紧急指针及选项字段。每个字段的作用和意义详尽说明,帮助理解TCP协议如何确保可靠的数据传输,是互联网通信的基石。通过学习这些内容,读者可以更好地掌握TCP的工作原理及其在网络中的应用。
|
3月前
|
网络协议 安全 网络安全
探索网络模型与协议:从OSI到HTTPs的原理解析
OSI七层网络模型和TCP/IP四层模型是理解和设计计算机网络的框架。OSI模型包括物理层、数据链路层、网络层、传输层、会话层、表示层和应用层,而TCP/IP模型则简化为链路层、网络层、传输层和 HTTPS协议基于HTTP并通过TLS/SSL加密数据,确保安全传输。其连接过程涉及TCP三次握手、SSL证书验证、对称密钥交换等步骤,以保障通信的安全性和完整性。数字信封技术使用非对称加密和数字证书确保数据的机密性和身份认证。 浏览器通过Https访问网站的过程包括输入网址、DNS解析、建立TCP连接、发送HTTPS请求、接收响应、验证证书和解析网页内容等步骤,确保用户与服务器之间的安全通信。
186 3
|
4月前
|
网络协议 物联网 数据处理
C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势
本文探讨了C语言在网络通信程序实现中的应用,介绍了网络通信的基本概念、C语言的特点及其在网络通信中的优势。文章详细讲解了使用C语言实现网络通信程序的基本步骤,包括TCP和UDP通信程序的实现,并讨论了关键技术、优化方法及未来发展趋势,旨在帮助读者掌握C语言在网络通信中的应用技巧。
88 2
|
4月前
|
监控 网络协议 网络性能优化
网络通信的核心选择:TCP与UDP协议深度解析
在网络通信领域,TCP(传输控制协议)和UDP(用户数据报协议)是两种基础且截然不同的传输层协议。它们各自的特点和适用场景对于网络工程师和开发者来说至关重要。本文将深入探讨TCP和UDP的核心区别,并分析它们在实际应用中的选择依据。
115 3
|
10月前
|
机器学习/深度学习 人工智能 网络协议
TCP/IP五层(或四层)模型,IP和TCP到底在哪层?
TCP/IP五层(或四层)模型,IP和TCP到底在哪层?
171 4
|
监控 网络协议 网络架构
IP协议【图解TCP/IP(笔记九)】
IP协议【图解TCP/IP(笔记九)】
196 0

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等