欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

基本TCP套接字编程

程序员文章站 2022-07-14 20:30:25
...

基本TCP套接字编程

并发服务器,它是在同时有大量的客户连接到同一服务器上时用于提供并发性的一种常用 Unix 技术。每个客户连接都迫使服务器为它派生(fork)一个新的进程。

基本TCP套接字编程

1. socket 函数

#include <sys/socket.h>

//返回:若成功则为非负描述符;若出错则为 -1
int socket(int family, int type, int protocol);

socket 函数在成功时返回一个小的非负整数值,它与文件描述符类似。称为 套接字描述符,简称 sockfd 。

family 参数

指明协议族。也称协议域。

type 参数

指明套接字类型。

protocol 参数

某个协议的类型常值。或者设为 0,以选择所给定 family 和 type 组合的系统默认值。

family 说明
AF_INET IPv4 协议
AF_INET6 IPv6 协议
AF_LOCAL Unix域协议
AF_ROUTE 路由套接字
AF_KEY **套接字
type 说明
SOCK_STREAM 字节流套接字
SOCK_DGRAM 数据报套接字
SOCK_SEQPACKET 有序分组套接字
SOCK_RAW 原始套接字
protocol 说明
IPPROTO_TCP TCP传输协议
IPPROTO_UDP UDP传输协议
IPPROTO_SCTP SCTP传输协议

socket函数中family和type参数的组合:

type\family AF_INET AF_INET6
SOCK_STREAM TCP/SCTP TCP/SCTP
SOCK_DGRAM UDP UDP
SOCK_SEQPACKET SCTP SCTP
SOCK_RAW IPv4 IPv6

2. connect 函数

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

#include <sys/socket.h>

//返回:若成功,返回 0;否则,返回 -1
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

socketfd 是由socket函数返回的套接字描述符。
servaddr 和 addrlen 分别是一个指向套接字地址结构的指针和该结构的大小。

在每次connect失败后,都必须close当前的套接字描述符并重新调用socket 。

3. bind 函数

bind 函数把一个本地协议地址赋予一个套接字。

#include <sys/socket.h>

//返回:若成功,返回 0;否则,返回 -1
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

myaddr 是一个指向特定于协议的地址结构的指针。addrlen 是该地址结构的长度。

对于 TCP ,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都制定,还可以都不指定。

  • 服务器在启动时捆绑它们的众所周知端口。
  • 进程可以把一个特定的IP地址捆绑到它的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一。

给bind函数指定要捆绑的IP地址或端口号产生的结果:

进程指定( IP地址、端口号 ) 结果
通配地址、 0 内核选择IP地址和端口
通配地址、非0 内核选择IP地址,进程指定端口
本地IP地址、0 进程指定IP地址,内核选择端口
本地IP地址、非0 进程指定IP地址和端口

对于 IPv4,通配地址由常值 INADDR_ANY 指定,其值一般为 0 。
例:

struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

如此赋值对IPv4是可行的,因为其IP地址是一个32位的值,可以用一个简单的数字常量值表示。
对于IPv6,就不能这样,因为128位的IPv6地址是存放在一个结构中的。

struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any;

系统预先分配in6addr_any变量并将其初始化为常值IN6ADDR_ANY_INIT 。
头文件

4. listen 函数

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

listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。

#include <sys/socket.h>

//返回:若成功,返回 0;若失败,返回 -1
int listen(int sockfd, int backlog);

backlog参数规定了内核应该为相应套接字排队的最大连接个数。

内核为任何一个给定的监听套接字维护两个队列:
- 未完成连接队列:每个这样的SYN分节对应其中一项,已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于SYN_RCVD 状态。
- 已完成连接队列:每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。

5. fork和exec函数

#include <unistd.h>

//返回:在子进程中为 0,在父进程中为子进程ID;若出错,返回 -1
pid_t fork(void);

网络服务器利用了这个特性:父进程调用 accept 之后调用 fork。所接受的已连接套接字随后就在父进程和子进程之间共享。

fork 有两个典型用法:
1. 一个进程创建一个自身的副本。
2. 一个进程想要执行另一个程序。

称调用exec的进程为调用进程,称新执行的程序为新程序。

#include <unistd.h>

//返回:若成功则不返回;若出错则为 -1

int execl(const char *pathname, const char *argv0, .../* (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 *pathname, const char *arg0, ... /* (char *)0 */);

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

6. 并发服务器

pid_t pid;
int listenfd, connfd;

listenfd = Socket(....);

bind(listenfd, ...);
listen(listenfd, LISTENQ);

for ( ; ; ) {
    connfd = accept(listedfd, ...); 

    if ((pid = fork()) == 0) {
        close(listenfd);    /* child closes listening socket */
        //....              /* process the request */
        close(connfd);

        exit(0);
    }

    close(connfd);
}

当该函数返回时,我们在子进程中显式地关闭已连接套接字。这一点非必需,因为下一个语句就是调用exit,而进程终止处理的部分工作就是关闭所有由内核打开的描述符。

7. close函数

#include <unistd.h>

//返回:若成功,返回0;若出错则为 -1
int close(int sockfd);

close一个套接字TCP套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。

描述符引用计数

并发服务器中父进程关闭已连接套接字只是导致相应描述符的引用计数值减 1 。

如果我们确实想在某个TCP连接上发送一个FIN,那么可以改用 shutdown 函数以代替close 。

8. getsockname和getpeername函数

#include <sys/socket.h>

//返回:若成功则为 0,若出错则为 -1

//返回与某个套接字关联的本地协议地址
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);

//返回与某个套接字关联的外地协议地址
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);