基本TCP套接字编程

GuangYing Lv1

一个完整的TCP客户/服务器程序所需要的基本套接字函数


socket函数

指定期望的通信协议类型

1
2
3
#include <sys/socket.h>
int socket(int family, int type, int protocol);
返回:若成功则为非负描述符,若出错则为-1

family参数指明协议族
type参数指明套接字类型
protocol参数为某个协议类型常值
或设为0以选择family和type组合的系统默认值

并非所有套接字family与type的组合都是有效的

socket函数成功时返回非负整数:套接字描述符,简称sockfd

AF_XXX和PF_XXX
AF_前缀表示地址族
PF_前缀表示协议族
历史上曾设想:单个协议族可支持多个地址族,PF_值用来创建套接字,AF_值用于套接字地址结构
但实际上支持多个地址族的协议族从未实现过,而且PF_值往往与此协议的AF_值相等

connect函数

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

1
2
3
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
返回:若成功则为0,若出错则为-1

sockfd:由socket函数返回的套接字描述符
servaddr:指向套接字地址结构的指针
addrlen:该结构的大小

客户在调用函数connect前不必非得调用bind函数
如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口

如果是TCP套接字,调用connect函数将激发TCP的三路握手过程
且仅在连接建立成功或出错时才返回
出错的可能情况:

  • TCP客户没有收到SYN分节的相应,则返回ETIMEDOUT错误
    • 即超时
  • 对客户的SYN的相应是RST(表示复位),则表明该服务器主机在指定端口上没有进程在等待与之连接
    • 是一种硬错误,客户一接收到RST就马上返回ECONNREFUSED错误
    • RST是TCP发生错误时发送的一种TCP分节
      • 产生条件:
        • 目的地为某端口的SYN到达,而该端口上没有正在监听的服务器
        • TCP想取消一个已有链接
        • TCP接收到一个根本不存在的连接上的分节
  • 客户发出的SYN在中间某个路由器上引发了一个destination unreachable(目的地不可达)ICMP错误
    • 则认为是一种软错误,客户主机内核保存该消息,并按第一种情况中所述的时间间隔继续发送SYN
    • 若在某个规定时间后仍未收到响应,则把保存的消息(ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程
    • 也可能是是以下两种情况:
      • 按照本地系统转发表,根本没有到达远程系统的路径
      • connect调用根本不等待就返回

若connect失败则该套接字不再可用,必须关闭,不能对这样的套接字再次调用connect函数

bind函数

把一个本地协议地址赋予一个套接字
对于网际网协议,协议地址是32位的IPv4地址 或 128位的IPv6地址与16位的TCP或UDP端口号的组合

1
2
3
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
返回:若成功则为0,若出错则为-1

myaddr:指向特定于协议的地址结构的指针
addrlen:该地址结构的长度

  • 服务器在启动时捆绑它们的众所周知端口
  • 进程可以把一个特定的IP地址捆绑到它的套接字上,但这个IP地址必须属于其所在主机的网络接口之一
    • 对于TCP,这就为在该套接字上发送的IP数据报指派了源IP地址
    • 且 限定该套接字只接收那些目的地为这个IP地址的客户连接
    • TCP客户通常不把IP地址捆绑到它的套接字上
      • 当连接套接字时,内核将根据所用外出网络接口来选择源IP地址,外出接口则取决于到达服务器所需的路径
      • 若TCP服务器没有把IP捆绑到套接字上,内核就把客户发送的SYN的目的地址作为服务器的源IP地址

调用bind可以指定IP地址或接口,可以两者都指定,也可以都不指定

若指定端口号为0,则内核在bind调用时选择临时端口
若指定IP地址为通配地址,内核等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址

IPv4通配地址由常值INADDR_ANY指定

1
2
struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any;

IPv6存放在一个结构中,于是使用in6addr_any
系统预先分配in6addr_any变量并初始化为常值IN6ADDR_ANY_INIT

1
2
struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any;

若让内核为套接字选择临时端口号,则bind不返回所选择的值
因为bind函数第二个参数有const限定
为此必须调用getsockname函数来返回协议地址

从bind函数返回的一个常见错误是EADDRINUSE(Address already in use,地址已使用)

listen函数

仅由TCP服务器调用,做两件事

  • 当socket函数创建一个套接字时,被假设为一个主动套接字:一个将调用connect发起连接的客户端套接字
    • listen函数把一个未连接的套接字转换成一个被调套接字,指示内核应接受指向该套接字的连接请求
  • 函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数
    1
    2
    3
    #include <sys/socket.h>
    int listen(int sockfd, int backlog);
    返回:若成功则为0.若出错则为-1
    通常在调用socket和bind后,调用accept前调用

不要把backlog定义为0,不同的实现对此有不同的解释
如果不想让任何客户连接到你的监听套接字上,那就关掉该监听套接字

if((ptr = getenv("LISTENQ")) != NULL)
backlog = atoi(ptr);
允许环境变量LISTENQ覆写由调用者指定的值
可由此改变程序指定的backlog值

accept函数

由TCP服务器调用
用于从已完成连接队列队头返回下一个已完成连接
如果已完成队列为空,则进程被投入睡眠(假定套接字为默认的阻塞方式)

1
2
3
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
返回:若成功则为非负描述符,若出错则为-1

dliaddr和addrlen:用来返回已连接的对端进程(客户)的协议地址
addrlen是值-结果参数
调用前先将*addrlen所引用的整数置为由cliaddr所指的套接字地址结构的长度
返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节

若accept成功,返回值为内核自动生成的新描述符,代表与所返回的客户的TCP连接
对于accept函数,称第一个参数为监听套接字描述符
称返回值为已连接套接字描述符

若对返回客户协议地址不感兴趣,可把cliaddr和addrlen均置为空指针

forkexec函数

Unix下的fork函数(包括各种变体)的Unix中派生新进程的唯一方法

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

调用一次 返回两次

fork两个经典用法:

  • 一个进程创建自身副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作
    • 网络服务器的典型用法
  • 一个进程想执行另一个程序
    • 先fork,在调用exec把自身替换为新的程序
    • 是shell之类的程序的典型用法

6个exec函数区别:

  • 待执行的程序文件是由文件名 还是 由路径名指定
  • 新程序的参数是一一列出 还是 由一个指针数组来引用
  • 把调用进程的环境传递给新程序 还是 给新程序指定新的环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>

int execl(const char *pathname, const char *arg0, ... /* (char *) 0 */ );

int execv(const char *pathname, char *const *argv[]);

int execle(const char *pathname, const char *arg0, ... /* (char *) 0, char *const envp[] */ );

int execve(const char *pathname, char *const argv[], char *const envp[]);

int execlp(const char *filename, const char *arg0, ... /* (char *) 0 */ );

int execvp(const char *filename, char *const argv[]);
均返回:若成功则不返回,若出错则为-1;

只有在出错时才返回到调用者
否则,控制将被传递给新程序的起始点,通常就是main函数

一般只有execve是内核中的系统调用,其他5个都是execve的库函数

进程在调用exec之前打开着的描述符通常跨exec继续保持打开
通常:因为本默认行为可以使fcntl设置的FD_CLOEXEC描述符标志禁止掉

并发服务器

典型并发服务器程序的轮廓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pid_t pid;
int lisetenfd, connfd;
listenfd = Socket(...);
Bind(listenfd, ...);
Listen(listenfd, LISTENQ);
for( ; ; ) {
connfd = Accept(listenfd, ...);
if ( (pid = Fork()) == 0) {
Close(listenfd);
doit(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}

TCP套接字调用close会导致发送一个FIN,随后是正常的TCP连接终止序列
父进程对connfd调用close没有终止它与客户的连接:
每个文件或套接字都有一个引用计数
fork返回后描述符被共享(复制),引用计数为2
引用计数为0时关闭文件

close函数

通常的Unix close函数也用来关闭套接字,并终止TCP连接

1
2
3
#include <unistd.h>
int close(int sockfd);
返回:若成功则为0,若出错则为-1

close一个TCP套接字的默认行为:

  • 把该套接字标记成已关闭,然后立即返回到调用进程
  • 该套接字描述符不能再由调用进程使用(即read或write的第一个参数)
  • 而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列

然而并发服务器中父进程关闭套接字只是导致相应描述符的引用计数值减1
若想在某个TCP连接上发送一个FIN,那么可改用shutdown函数代替close

若父进程不close套接字,将会耗尽套接字资源

getsocknamegetpeername函数

getsockname:返回与某个套接字关联的本地协议地址
getpeername:返回与某个套接字关联的外地协议地址

1
2
3
4
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
均返回:若成功则为0,若出错则为-1

最后一个参数都是值-结果参数
两个函数都得装填由localaddr或peeraddr指针所指的套接字地址结构

需要这两个函数的理由:

  • 在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号
  • 在以端口号0调用bind(告知内核去选择本地端口号)后,getsockname用于返回由内核赋予的本地端口号
  • getsockname可用于获取某个套接字的地址族
  • 在一个以通配IP地址调用bind的TCP服务器上,与某个客户的连接一旦建立(accept成功返回)
    • getsockname就可以用于返回由内核赋予该连接的本地IP地址
    • 在这样的调用中,套接字描述符参数必须是已连接套接字的描述符,而不是监听套接字的描述符
  • 当一个服务区是由调用过accept的某个进程调用exec执行程序时,它能够获取客户身份的唯一途径就是调用getpeername
  • 标题: 基本TCP套接字编程
  • 作者: GuangYing
  • 创建于 : 2024-12-25 14:48:04
  • 更新于 : 2024-12-25 20:36:11
  • 链接: http://quebo.cn/2024/12/25/基本TCP套接字编程/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。