介绍
本节主要介绍网络编程的理论,从服务端/客户端示例来解析网络编程常用的方式,使用到的一些方法,其中包括:socket、connect、bind、listen、accept、close。
图1是client与server间交互最常见的一种场景:
- server启动,绑定指定端口,开始监听
- client启动并连接上server
- client向server发送消息
- server端接收到消息进行处理
- server返回响应给client端
- client接收消息
- client若继续交互,则重复3-6步骤
- client执行close,发送结束标记给server
- server接收到结束标记,执行close,结束交互
客户端和服务端
服务端示例代码如下:
/home/unpv13e-master/intro/daytimetcpsrv.c
#include "unp.h"
#include <time.h>
int
main(int argc, char **argv)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
time_t ticks;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13); /* daytime server */
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
connfd = Accept(listenfd, (SA *) NULL, NULL);
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff));
Close(connfd);
}
}
客户端代码示例如下:
/home/unpv13e-master/intro/daytimetcpcli.c(本示例改动了部分代码)
#include <unp.h>
#include "apue.h"
int main(int argc, char **argv)
{
int sockfd,n;
char recvline[MAXLINE+1];
struct sockaddr_in servaddr;
if (argc != 2)
{
err_quit("usage: a.out <IPaddress>");
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0))<0)
{
err_sys("socket error");
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13);
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
{
err_quit("inet_pton error for %s", argv[1]);
}
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
{
err_sys("connect error");
}
while ((n = read(sockfd, recvline, MAXLINE)) > 0)
{
recvline[n] = 0;
if (fputs(recvline, stdout) == EOF)
{
err_sys("fputs error");
}
}
if (n < 0)
{
err_sys("read error");
}
exit(0);
}
运行结果如下:
#服务端启动
sudo ./daytimetcpsrv
#客户端启动
sudo ./daytimetcpcli 127.0.0.1
#运行结果
Fri Jul 22 14:10:35 2022
socket
对于client/server,它们启动的第一步都是执行socket方法。
/* Create a new socket of type TYPE in domain DOMAIN, using
protocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically.
Returns a file descriptor for the new socket, or -1 for errors. */
extern int socket (int __domain, int __type, int __protocol) __THROW;
参数说明
__domain
也就是protocol family,协议簇
__domain | 描述 |
---|---|
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | unix协议 |
AF_ROUTE | 路由socket |
AF_KEY | key socket |
__type
待创建的socket类型
__type | 描述 |
---|---|
SOCK_STREAM | stream socket |
SOCK_DGRAM | datagram socket |
SOKC_SEQPACKET | sequenced packet socket |
SOCK_RAW | raw socket |
__protocol
socket协议类型
__protocol | 描述 |
---|---|
IPPROTO_TCP | TCP协议 |
IPPROTO_UDP | UDP协议 |
IPPROTO_SCTP | SCTP协议 |
三者组合
支持 | AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY |
---|---|---|---|---|---|
SOCK_STREAM | TCP|SCTP | TCP|SCTP | Yes | ||
SOCK_DGRAM | UDP | UDP | Yes | ||
SOKC_SEQPACKET | SCTP | SCTP | Yes | ||
SOCK_RAW | IPv4 | IPv6 | Yes | Yes |
返回值
返回值 | 说明 |
---|---|
非负整数 | fd(socket对应的文件描述符) |
-1 | 执行失败 |
作用
为了执行网络IO,进程第一步要做的事就是调用socket方法,指定协议项(tcp/udp/sctp,ipv4/ipv6 ...)
bind
struct sockaddr
{
usigned short int sa_family; /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
/* Give the socket FD the local address ADDR (which is LEN bytes long). */
extern int bind (int sockfd, const struct sockaddr *myaddr, socklen_t addrlen) __THROW;
参数说明
sockfd:为socket()方法返回的fd myaddr:socket address的指针(IP和端口信息) addrlen:struct sockaddr的长度
返回值
返回值 | 说明 |
---|---|
0 | 执行成功 |
-1 | 执行失败 |
作用
给socket分配一个协议地址,socket与端口绑定
端口如果为0,则由kernel来决定端口号
ip如果为0,则由kernel来决定ip
对于由kernel决定的信息,可以通过getsockname方法来获取。
对于服务端来说,我们需要指定一个端口号,客户端需要知道ip和port才能访问该服务。服务端是对外提供服务的,访问地址和端口信息是公开的。
但也有例外,比如注册发现机制的服务端来说,是不需要指定端口。
对于客户端来说,是无需执行bind操作的。
注:对于服务端,在绑定时,如果未指定一个ip地址,它会在接收到客户端发送SYN信息时,将SYN信息中的ip地址作为服务端的ip地址。
connect
/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
For connectionless socket types, just set the default address to send to
and the only address from which to accept transmissions.
Return 0 on success, -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int connect (int sockfd, const struct sockaddr *servaddr, socklen_t __len);
参数说明
参数同bind
但这里的第二个参数,是远程的ip和端口信息
返回值
返回值 | 说明 |
---|---|
0 | 执行成功 |
-1 | 执行失败 |
作用
该方法会在连接建立或异常时,才会返回
对于tcp连接来说,过程包括:三次握手
- client发送SYN后,未收到服务端的<SYN, ACK>, ETIMEDOUT
- 如果服务端收到SYN后,响应一个RST,表示服务端没有处理该连接,客户端此时会有ECONNREFUSED错误返回
- 请求的端口无对应服务
- tcp剔除对应连接
- tcp收到一个不存在的连接片段信息
- client收到ICMP不可达信息,它会继续尝试发送SYN,如果在固定的超时时间内,没有返回信息,它会返回EHOSTUNREACH或者ENETUNREACH
listen
/* Prepare to accept connections on socket FD.
N connection requests will be queued before further requests are refused.
Returns 0 on success, -1 for errors. */
extern int listen (int sockfd, int backlog) __THROW;
参数说明
- sockfd: socket-fd,socket描述符
kernel会为任何一个给定的监听socket维护两个队列(如下图):
- 未完成连接队列
- 已完成连接队列
返回值
返回值 | 说明 |
---|---|
0 | 执行成功 |
-1 | 执行失败 |
作用
socket创建的套接字,默认是一个主动套接字,也就是我们所说的客户端套接字,将会调用connect方法的套接字。
listen可以将主动套接字转换成被动套接字,指示kernel应接受指示该套接字的连接请求。tcp状态由CLOSED转换成LISTEN
backlog的作用:当队列长度超过该设置值时,tcp连接请求将会被忽略。但tcp会重试发送请求,很可能后面就会接受该tcp请求。
accept
/* Await a connection on socket FD.
When a connection arrives, open a new socket to communicate with it,
set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
peer and *ADDR_LEN to the address's actual length, and return the
new socket's descriptor, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int accept (int __fd, const struct sockaddr cliaddr,
socklen_t *__restrict addrlen);
参数说明
- cliaddr: 已连接的客户端地址信息
- addrlen: cliaddr长度
返回值
返回值 | 说明 |
---|---|
非负整数 | 已建立连接的描述符 |
-1 | 执行失败 |
作用
返回下一个已完成的连接(从连接队列队首获取),如果连接队列为空,该方法会阻塞等待。
返回值是一个已建立连接的描述符,该描述符是由kernel自动创建。失败会返回异常值
close
#include <unistd.h>
/* Close the file descriptor FD.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int close (int sockfd);
参数说明
- sockfd: 待关闭的socket的描述符
返回值
返回值 | 说明 |
---|---|
0 | 执行成功 |
-1 | 执行失败 |
作用
标记socket为closed状态,对应的描述符不能再被进程使用,即:不能被read/write使用。但如果缓冲中还有数据,会被发送到对应端。