基本TCP套接口编程

基本TCP套接口编程

TCP客户端-服务器典型事件

下图是TCP客户端与服务器之间交互的一系列典型事件时间表:

  1. 首先启动服务器,等待客户端连接
  2. 启动客户端,连接到服务器
  3. 客户端发送一个请求给服务器,服务器处理请求,响应客户端
  4. 循环步骤3
  5. 客户端给服务器发一个文件结束符,关闭客户端连接
  6. 服务器也关闭连接
基本TCP客户-服务器程序的套接口函数

套接口编程基本函数

socket 函数

为了执行网络I/O,一个进程(无论是服务端还是客户端)必须做的第一件事情就是调用socket函数。

#include <sys/socket.h> /* basic socket definitions */
int socket(int family, int type, int protocol);/* 返回:非负描述字——成功,-1——出错 */
  • family——协议族

解释

AF_INET

IPv4协议

AF_INET6

IPv6协议

AF_LOCAL

Unix域协议

AF_ROUTE

路由套接口

AF_KEY

密钥套接口

  • type——套接口类型

类型

解释

SOCK_STREAM

字节流套接口

SOCK_DGRAM

数据报套接口

SOCK_RAW

原始套接口

下面是有效的familytype组合(简略版):

AF_INET

AF_INET6

SOCK_STREAM

TCP

TCP

SOCK_DGRAM

UDP

UDP

SOCK_RAW

IPv4

IPv6

socket函数返回一个套接口描述字,简称套接字(sockfd)。获取套接字无需指定地址,只需要指定协议族和套接口类型(如上表中的组合)。

connect函数

TCP客户用connect函数来建立一个与TCP服务器的连接。

#include <sys/socket.h> /* basic socket definitions */
int connect(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);/* 返回:0——成功,-1——出错 */
  • 参数sockfd便是socket函数返回的套接口描述字。
  • 套接口地址结构servaddr必须包含服务器的IP地址和端口号。
  • 客户端不必非要绑定一个端口(调用bind函数),内核会选择源IP和一个临时端口。
  • connect函数会触发TCP三次握手。有可能出现下面的错误情况:

1.客户端未收到SYN分节的响应

第一次发出未收到,间隔6s再发一次,再没收到,隔24秒再发一次,总共等待75s还没收到则返回错误( ETIMEDOUT)。可以用时间日期程序验证一下:

查看本地网络信息:

JACKIELUO-MC0:intro jackieluo$ ifconfig
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	ether f4:0f:24:2a:72:a6
	inet6 fe80::1830:dbd:1b29:2989%en0 prefixlen 64 secured scopeid 0x6
	inet 192.168.0.101 netmask 0xffffff00 broadcast 192.168.0.255
	nd6 options=201<PERFORMNUD,DAD>
	media: autoselect
	status: active

将程序指向本地地址192.168.0.101(确保时间日期服务器程序已运行),成功:

JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.101
Sat Oct  6 17:06:55 2018

将程序指向本地子网地址192.168.0.102,其主机ID(102)不存在,等待几分钟后超时返回:

JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.102
connect error: Operation timed out

2.收到RST

即服务器主机在指定端口上没有等待连接的进程,这称为“hard error”,客户端一接收到RST,马上返回错误(ECONNREFUSED)。验证:

关闭之前本机运行的daytimetcpsrv进程

将程序指向本地地址192.168.0.101

JACKIELUO-MC0:intro jackieluo$ ./daytimetcpcli 192.168.0.101
connect error: Connection refused

3.发出的SYN在路由器上引发了目的不可达ICMP错误

这个错误被称为“soft error”,最终返回EHOSTUNREACH或者ENETUNREACH

bind函数

函数bind为套接口分配一个本地协议地址,包括IP地址和端口号。

#include <sys/socket.h> /* basic socket definitions */
int bind(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);/* 返回:0——成功,-1——出错 */
  • 客户端可以不调用这个函数,由内核选择一个本地ip的临时端口就好。
  • 服务器一般都会调用bind函数绑定ip地址和端口,供客户端调用。一个例外是RPC(远程过程调用)服务器,它由内核为其选择临时端口。然后通过RPC端口映射器进行注册,客户端与该服务器连接之前,先通过端口映射器获取服务器的端口。
  • 进程可以把一个特定的IP地址捆绑到它的套接口上。对于客户端,它发送的请求,源IP地址就是这个地址;对于服务器,如果绑定了IP地址,则只接受目的地为此IP地址的客户连接。
  • 如果服务器不把IP地址绑定到套接口上,那么内核把客户端发送SYN所在分组的目的IP地址作为服务器的源IP地址。(即服务器收到SYN的IP)

给函数bind指定用于捆绑的IP地址和/或端口号的结果:

IP地址

端口

结果

0

内核选择IP地址和端口

非0

内核选择IP地址,进程指定端口

本地IP地址

0

进程选择IP地址,内核指定端口

本地IP地址

非0

进程选择IP地址和端口

listen函数

函数listen仅被TCP服务器调用。

#include <sys/socket.h> /* basic socket definitions */
int listen(int sockfd, int backlog);/* 返回:0——成功,-1——出错 */

调用函数socket函数创建的套接口,默认是主动方,下一步应是调用connectCLOSED的下一个状态是SYN_SENT(见TCP状态转换图)。而函数listen将套接口转换成被动方,告诉内核,应接受指向此套接口的连接请求,CLOSED状态变成LISTEN

函数listen的第二个参数backlog表示内核为此套接口排队的最大连接数。对于给定的监听套接口,内核会维护两个队列:

  1. 未完成连接队列(incomplete connection queue) SYN分节已由客户发出,到达服务器,正在进行TCP的三路握手。此时这些套接口处于SYN_RCVD状态。
  2. 已完成连接队列(completed connection queue) SYN分节已由客户发出,到达服务器,并且已完成三路握手。此时这些套接口处于ESTABLISHED状态。
  3. 当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新条目,直到三路握手中,第三个分节(客户对服务SYN的ACK)到达,这个条目移到已完成连接队列的队尾。
  4. 当进程调用accept函数时,已完成连接队列的头部条目返回给进程。
  5. 两个队列之和不能超过backlog
  6. 当一个客户SYN到达时,若这两个队列都是满的,TCP就忽略此分节,且不发送RST。客户TCP将重发SYN,期望不久就能在队列中找到空闲位置。
    TCP为监听套接口维护的两个队列

accept函数

函数accept由TCP服务器调用,从已完成连接队列头部返回下一个已完成连接,若该队列为空,则进程睡眠(假定套接口为默认的阻塞方式)。

#include <sys/socket.h> /* basic socket definitions */
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);/* 返回:非负描述字——成功,-1——出错 */

函数accept的第一个参数和返回值都是套接口描述字。其中,

  1. 第一个参数,称为监听套接口描述字,即由函数socket返回,也用于bindlisten的第一个参数。
  2. 返回值,称为已连接套接口描述字。

通常一个服务器,只生成一个监听套接口描述字,直到其关闭。而内核为每个被接受的客户连接,创建一个已连接套接口,当客户连接完成时,关闭该已连接套接口。

注意到intro/daytimetcpsrv.c中,后两个参数传的都是空指针,这是因为我们不关注客户的身份,无需知道客户的协议地址。

connfd = Accept(listenfd, (SA *) NULL, NULL);

稍作修改,不再传入空指针,见intro/daytimetcpsrv1.c

socklen_t len;
struct sockaddr_in servaddr, cliaddr;
...
connfd = Accept(listenfd, (SA *) &cliaddr, &len);
printf("connection from %s, port %d\n",
    Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
    ntohs(cliaddr.sin_port));

kill掉之前的daytimetcpsrv进程:

$ sudo lsof -i -P | grep -i "listen"
daytimetc 80986           root    3u  IPv4 0xae12d925e4528793      0t0    TCP *:13 (LISTEN)
$ sudo kill -9 80986

编译运行新的服务端程序:

$ make daytimetcpsrv1.c daytimetcpsrv1
$ ./daytimetcpsrv1

重复执行客户端程序,发几个请求:

$ ./daytimetcpcli 127.0.0.1
Wed Sep 26 14:11:20 2018
$ ./daytimetcpcli 127.0.0.1
Wed Sep 26 14:17:06 2018

查看服务端打印:

connection from 127.0.0.1, port 58201
connection from 127.0.0.1, port 58342

注意到,由于客户端程序没有调用bind函数,内核为它的协议地址选择了源ip作为IP地址,临时端口号也发生了变化。

fork和exec函数

#include <unistd.h>
pid_t fork(void);/* 返回:在子进程中为0,在父进程中为子进程ID,-1——出错 */

fork函数调用一次,却返回两次。

  1. 在调用它的进程(即父进程),它返回一次,返回值是派生出来的子进程的进程ID。 父进程可能有很多子进程,必须通过返回值跟踪记录子进程ID。
  2. 在子进程,它还返回一次,返回值为0。 子进程只有一个父进程,总可以通过getppid来得到父进程的ID

通过返回值可以判断当前进程是子进程还是父进程。

父进程在调用fork之前打开的所有描述字在函数fork返回后都是共享的。网络服务器会利用这一特性:

  1. 父进程调用accept
  2. 父进程调用fork,已连接套接口就在父进程与子进程间共享。(一般来说就是子进程读、写已连接套接口,而父进程关闭已连接套接口)。

fork有两个典型应用:

  1. 一个进程为自己派生一个拷贝,并发执行任务,这也是典型的并发网络服务器模型。
  2. 一个进程想执行其他的程序,于是调用fork生成一个拷贝,利用子进程调用exec来执行新的程序。典型应用是shell。

以文件形式存储在硬盘上的可执行程序若要被执行,需要由一个现有进程调用exec函数。我们将调用exec的进程称为调用进程,新程序的进程ID并不改变,仍处于当前进程。

小结

客户和服务器,从调用socket开始,返回一个套接口描述字。客户调用connect,服务器调用bindlistenaccept。最后套接口由close关闭。

多数TCP服务器是调用fork来实现并发处理多客户请求的。多数UDP服务器则是迭代的。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏奔跑的蛙牛技术博客

Java并发知识点(2)

用new操作符创建一个新线程时如new Thread(r)  该线程还没有被运行,这意味着这个线程的状态是new。当一个线程处于新创建状态,程序还没有开始运行线...

10020
来自专栏编程

小白爬虫之爬虫快跑

使用多线程时好像在目录切换的问题上存在问题,可以给线程加个锁试试 Hello 大家好!我又来了。 你是不是发现下载图片速度特别慢、难以忍受啊!对于这种问题一般解...

20380
来自专栏java学习

面试题13(一个具有生命的线程有哪些状态)

考点:考察求职者对线程的理解 出现频率:★★★ 【面试题解析】线程的状态表示线程在某时间段内进行的活动和将要进行的任务程有创建、就绪、运行、阻塞、死亡5种状态。...

34550
来自专栏运维一切

ETCD的基本使用 转

原文地址: http://www.csdn.net/article/2015-01-22/2823659

15230
来自专栏醒者呆

程序员必备课程——网络编程入门

关键字:互联网协议,网络分层,socket,TCP/IP协议,jdk源码,多线程,线程池,ExecutorService 本文的主要目的是面向程序员,所以...

58260
来自专栏HansBug's Lab

【前端】Ubuntu16下nodejs+npm+vue环境配置

笔者最近在学习vue.js,不过一直都是在runoob上面各种尝试。今天笔者在本机(Ubuntu16.04)尝试部署了nodejs+npm+vue开发环境,接下...

38150
来自专栏码洞

天下无难试之HTTP协议面试刁难大全

小编是一个非典型面试官,对于HTTP协议的第一个问题,一般人会问常用的状态码有哪些。小编不这么问,小编的问题是HTTP的全称是什么,把英语给我说出来!

11610
来自专栏张尧博客

su和sudo之间的区别以及如何在Linux中配置sudo

54360
来自专栏AndroidTv

记录 FTPClient 超时处理的相关问题问题源码跟进结论常见异常

apache 有个开源库:commons-net,这个开源库中包括了各种基础的网络工具类,我使用了这个开源库中的 FTP 工具。

29620
来自专栏惨绿少年

rsync 服务部署详解

第1章 rsync 软件介绍 1.1 什么是rsync rsync 是一款开源的、快速的、多功能的、可实现全量及增量的本地或远程数据同步备份的优秀工具。 ht...

49300

扫码关注云+社区

领取腾讯云代金券