第2章 基本的TCP套接字
2.1 IPv4 TCP客户端
4个步骤:
(1) socket()创建TCP套接字(window下要用初始化套接字环境)
(2) connect()建立到达服务起的连接
(3) send()和recv() 通信
(4) close关闭连接(Windows 下使用closesock())
2.1.1 应答(echo)协议的客户端
程序代码如下:
文件:practical.h,错误处理函数
#ifndef __PRACTICAL_H__ #define __PRACTICAL_H__ #define MAXLINE 4096 /* max line length */ extern void err_sys(const char *fmt, ...); extern void err_quit(const char *fmt, ...); #endif /* practical.h */ 文件:practical.c #include "practical.h" #include <stdarg.h> /* ISO C varibale arguments: va_list, va_start(), va_end()*/ #include <stdio.h> /* vsnprintf(), snprintf(), fputs(), fflush() */ #include <errno.h> /* errno */ #include <string.h> /* strcat(), strerror(), strlen()*/ #include <stdlib.h> /* exit() */ /* * Print a message and return to caller. * Caller specifies "errnoflag." */ static void err_doit(int errnoflag, int error, const char *fmt, va_list ap) { char buf[MAXLINE]; vsnprintf(buf, MAXLINE, fmt, ap); if (errnoflag) { snprintf(buf + strlen(buf), MAXLINE - strlen(buf), ": %s", strerror(error)); } strcat(buf, "\n"); fflush(stdout); /* in case stdout and strerr are the same*/ fputs(buf, stderr); fflush(NULL); /* flushes all stdio output streams */ } /* * Fatal error related to a system call. * Print a message and terminate. */ void err_sys(const char *fmt, ...) { va_list ap; va_start(ap, fmt); err_doit(1, errno, fmt, ap); va_end(ap); exit(1); } /* * Fatal error unrelated to a system call. * Print a message and terminate. */ void err_quit(const char *fmt, ...) { va_list ap; va_start(ap, fmt); err_doit(0, 0, fmt, ap); va_end(ap); exit(1); }
文件:tcp_echo_client.c
#include <stdio.h> /* fputs */ #include <stdlib.h> /* exit, atoi, memset */ #include <unistd.h> /* standard symbolic constants and types and miscellaneous functions: _POSIX_VERSION, _XOPEN_VERSION, R_OK, W_OK, SEEK_SET, SEED_CUR, F_LOCK, STDIN_FILENO, function declarations and so on */ #include <sys/types.h> /* system data types: clock_t, dev_t,gid_t, mode_t, off_t, pid_t, size_t, ssize_t, time_t, uid_t and so on*/ #include <sys/socket.h> /* socket, connect, bind, listen, accept, send, sendto, sendmsg, recv, recvform, recvmsg */ #include <netinet/in.h> /* socket address:sockaddr, sockaddr_in sockaddr_in6 and so on or old system: difine htons, htonl, ntohs, ntohl*/ #include <arpa/inet.h> /*inet_ntop, inet_pton, htons, htonl, ntohs, ntohl */ #include "practical.h" int main(int argc, char *argv[]) { //Test for correct number of arguments if (argc < 3 || argc > 4) { err_quit("Usage: a.exe <server address> <echo word> [<server port>]"); } char *servip = argv[1]; //first arg: server IP address(dotted quad) char *echo_string = argv[2]; //second arg: string to echo //Third arg(optional): server prot(numeric). 7 is well_known echo port in_port_t servport = (argc == 4) ? atoi(argv[3]) : 7; //Create a reliable, stream socket using TCP int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock < 0) { err_sys("socket failed"); } //Construct the server address structure struct sockaddr_in servaddr; //IPv4 server address memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; //IPv4 address family //Convert address int rtnval = inet_pton(AF_INET, servip, &servaddr.sin_addr.s_addr); if (rtnval == 0) { err_quit("inet_pton failed: invalid address string"); } else if (rtnval < 0) { err_sys("inet_pton failed"); } servaddr.sin_port = htons(servport); //server port //Establish the connection to the echo server if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) { err_sys("connect failed"); } size_t echo_stringlen = strlen(echo_string); //Send the string to the server ssize_t numbytes = send(sock, echo_string, echo_stringlen, 0); if (numbytes < 0) { err_sys("send() failed"); } else if (numbytes != echo_stringlen) { err_quit("send(), send unexpected of bytes"); } //Receive the same string back from the server unsigned int totalbytesrecvd = 0; //count of total bytes received fputs("Received: ", stdout); while (totalbytesrecvd < echo_stringlen) { char buf[BUFSIZ]; //I/O buffer //Receive up to the buffer size (minus 1 to leave space for a //null terminator) bytes from the sender numbytes = recv(sock, buf, BUFSIZ - 1, 0); if (numbytes < 0) { err_sys("recv() failed"); } else if (numbytes == 0){ err_quit("connection closed prematurely"); } totalbytesrecvd += numbytes; buf[numbytes] = '\0'; fputs(buf, stdout); } fputs("\n", stdout); close(sock); exit(0); }
注意:TCP是一种字节流协议,它的一种实现是不会保持send()边界。通过在连接的一端调用send()发送的字节可能不会通过在另一端单独调用一次recv而全部返回,需要反复接受。
编写套接字应用程序基本原则:对于另一端的网络和程序将要做什么事情,永远不要做出假设。
在给用户提示信息是,若需要格式化则使用printf(),否则使用fputs()。应该避免使用printf输出固定的,预先格式化的字符串。你从来都不应该把从网络收到的文本作为第一个参数传递给printf(),这会引起严重的安全问题,要用fputs()。
2.2 IPv4 TCP服务器
服务器职责是建立通信端点,被动等待客户的连接。
(1)socket()创建TCP套接字
(2)利用bind()给套接字分配端口
(3)listen()告诉系统允许对端口建立连接
(4)反复执行以下操作:
a.调用accept为每个客户连接获取新的套接字
b.使用send()和recv()通过新的套接字与客户通信
c.使用close()关闭客户连接。
程序代码如下:
文件:tcp_echo_server.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include "practical.h" void handle_tcp_client(int); static const int MAXPENDING = 5; //maxinum outstanding connection requsts int main(int argc, int argv[]) { if (argc != 2) { err_quit("Usage: a.exe <server prot>"); } in_port_t servport = atoi(argv[1]); //first arg: local port //Create socket for incoming connections int servsock; if ((servsock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))< 0) { err_sys("socket() failed"); } //Construct local address structure struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //Any incoming interface servaddr.sin_port = htons(servport); //Local port //Bind to the local address if (bind(servsock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) { err_sys("bind() failed"); } //Make the socket so it will listen to incoming connections if (listen(servsock, MAXPENDING) < 0) { err_sys("listen() failed"); } for (;;) { struct sockaddr_in clntaddr; //Set length of client address structure(in-out parameter) socklen_t clntaddrlen = sizeof(clntaddr); //Wait for a client to connect int clntsock = accept(servsock, (struct sockaddr*)&clntaddr, &clntaddrlen); if (clntsock < 0) { err_sys("accept() failed"); } //Clntsock is connected to a client char clntname[INET_ADDRSTRLEN]; //String to contain client address IPv4 if (inet_ntop(AF_INET, &clntaddr.sin_addr.s_addr, clntname, sizeof(clntname)) != NULL) { printf("Handling client %s/%d\n", clntname, ntohs(clntaddr.sin_port)); } else { fputs("Unable to get client address", stdout); } handle_tcp_client(clntsock); } //end while(true) } void handle_tcp_client(int clntsock) { char buf[BUFSIZ]; //Buffer for echo string //Reveive message from client ssize_t num_recvd = recv(clntsock, buf, BUFSIZ, 0); if (num_recvd < 0) { err_sys("recv() failed"); } //Send received string and receice again until end of stream while (num_recvd > 0) { //Echo message back to client ssize_t num_sent = send(clntsock, buf, num_recvd, 0); if (num_sent < 0) { err_sys("send failed"); } else if (num_sent != num_recvd){ err_sys("send(): sent unexpected number of bytes"); } //See if there is more data to receice num_recvd = recv(clntsock, buf, BUFSIZ, 0); if (num_recvd < 0) { err_sys("recv() failed"); } close(clntsock); //Close client socket } }
运行时:windows 下建议安装Cygwin,配置vim
gcc practical.c tcp_echo_client.c -o client 生成client
gcc practical.c tcp_echo_server -o server 生成server
先运行:server.out 端口号(如:5000)
client IPv4地址 显示字符串 端口号(5000)。
与客户端的不同:服务器中套接字的使用必须涉及一个地方绑定到套接字,然后把该套接字用作获得连接到客户的其他套接字的方式。客户必须给connect提供服务器的地址,而服务器必须给bind()指定它自己的地址。他们要对这份信息(服务器的地址和端口)达成协议进行通信,它们实际上都不需要知道客户的地址。
编写套接字网络应用程序的关键是:防御型编程,你的代码绝对不能对通过网络接受到的任何信息做出假设。
可以使用telnet 连接到服务器,telnet IP地址。
2.3 创建和销毁套接字
套接字是通信端点的抽象,访问套接字需要套接字描述符,在unix下是用文件描述符实现的。许多处理文件描述符的函数(read, write等)可以处理套接字描述符。
#include <sys/socket.h>
int socket(int domain, int type, int protocol)
返回值:成功返回文件(套接字)描述符,失败返回-1
用于创建套接字的实例。
domain:套接字的通信领域,
AF_INET IPv4
AF_INET6 IPv6
AF_UNIX UNIX域(AF_LOCAL域)
AF_UNSPEC 任何域
type:套接字类型,进一步确定通信特征
SOCK_DGRAM 长度固定的,无连接的不可靠报文传递,UDP默认协议
SOCK_RAW IP协议的数据报接口(POSIX.1中可选)
SOCK_SEQPACKET 长度固定有序,可靠的面向连接报文传递
SOCK_STREAM 有序,可靠,双向的面向连接字节流,TCP默认协议
protocol:端到端协议,通常为0,代表默认。SOCK_SEQPACKAGE与SOCK_STREAM类似,但从套接字得到的是基于报文服务的而不是字节流服务。这意味着它的套接字接收的数据量与对方发送的一致。流控制传输协议(Stream Control Transmission Protocol, SCTP)提供顺序数据报服务。SOCK_RAW套接字提供一个数据报接口用于直接访问下面的网络层(IP层)。使用这个接口时,应用程序负责构造自己的协议首部,传输协议(TCP, UDP)被绕过。当创建一个原始套接字时需要超级用户特权,用以防止恶意程序绕过内建安全机制来创建报文。
调用socket类似于open,在两种情况下均可以获得用于输入输出的文件描述符。不需要描述符时,调用close,释放该描述符以便重新使用,套接字描述符本质上一个文件描述符,但不是所有参数为文件的函数都可以接受套接字描述符。
关闭套接字
#include <sys/socket.h>
int close(int socket);
函数告诉底层协议栈发起关闭通信以及释放与套接字关联的资源。
套接字关闭是双向的,使用
#include <sys/socket.h>
int shutdown(int sockfd, int how);
返回值:成功0,失败-1
禁止套接字上的输入输出。
how: SHUT_RD,关闭读端,无法从套接字读取数据。
SHUT_WR ,关闭写端,无法从套接字发送数据。
SHUT_RDWR, 关闭读写。
2.4 指定地址
要确定通信目标进程,使用网络地址确定想要与之通信的计算机,服务(端口号)标识计算机上特定进程。
字节序列:大端法,低位地址标识高位字节(类似于我们写的数字,地址由低到高,左边为高位)。小端法相反,低位地址表示低位字节。(inter平台,小端,Power PC,SUN大端)
网络字节序列使用大端法,因此需要处理器字节序列与网络字节序列的转换。
#include <arpa/inet.h> //某些老系统<netinet/in.h>
uint32_t htonl(uint32_t hostint32); //主机字节序列转换到网络字节序列long int(32位)
返回值:以网络字节序列表示的32位整形
uint16_t htons(uint16_t hostint16); //主机字节序列转换到网络字节序列short int(16位),常用于转换Port号
返回值:以网络字节序列表示的16位整形
uint32_t ntohl(uint32_t hostint32); //网络字节序列long int(32位)转换到 主机字节序列
返回值:以主机字节序列表示的32位整形
uint16_t ntohl(uint16_t hostint16); //网络字节序列long short(16位)转换到 主机字节序列
返回值:以主机字节序列表示的16位整形
2.4.1 通用地址
以一个泛型地址用于传递给套接字函数。其它地址必须强制转换为它。
#include <netinet/in.h> //定义了所有地址
struct socketaddr {
sa_family_t sa_family; //Address family (e.g., AF_INET(6))
char sa_data[]; //variable-address length
}
套接字实现可以自由的添加额为的成员和定义sa_data[]的大小。
在Linux下,
struct socketaddr {
sa_family_t sa_family;
char sa_data[14];
}
FreeBSD下,
struct socketaddr {
unsigned int sa_len; //total length
sa_family_t sa_family;
char sa_data[14];
}
2.4.2 IPv4地址
sockaddr的结构依赖于IP版本。
struct in_addr {
in_addr_t s_addr; //IP address
}
struct sockaddr_in{
sa_family_t sin_family; //internet procotol (AF_INET)
in_port_t sin_port; //address port (16 bits)
struct in_addr sin_addr; //IPv4 address (32 bits)
char sin_zero[8 ]; //not used
}
in_port_t 是uint16_t, in_addr_t是uint32_t,都被定义在<stdint.h>中
sockaddr_in只是sockaddr结构的更详细视图,是为IPv4套接字定制的。把sockaddr_in强制转换为sockaddr时,套接字函数会检测sa_familiy字段获取实际类型,然后强制转换为合适类型。
2.4.3 IPv6地址
struct in6_addr {
uint8_t s_addr[16];
}
struct sockaddr_in6 {
sa_family_t sin6_family; //AF_INET6
in_port_t sin6_port;
uint32_t sin6_flowinfo; //traffic class and flow information
struct in6_addr sin6_addr; //IPv6 address (128 bits)
uint32_t sin6_scope_id; //set of interface of scope
}
2.4.4 通用地址存储器
sockaddr不足以存放sockaddr_in6,在我们想为一个地址结构分配大小时,但是不知道实际类型(4或6),sockaddr不工作,因为它对与某些结构大小。使用sockaddr_storage结构,保证与任何支持的地址类型一样大。
struct sockaddr_storage {
sa_family_t //前导地址组字段,确定地址的实际类型
...
}
在一些系统,地址结构包含一个额外的存储地址结构长度(字节为单位)的字段。由于长度字段并非所有系统都可用,应该避免使用,通常系统会提供一个值用于测试长度是否存在。
2.4.5二进制/字符串地址转换
套接字函数只能理解数字(二进制形式),但是人们使用的是“可打印字符串(如:192.178.2.2,1::1)”。
#include <arpa/inet.h>
const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socket_t size);
返回值:成功返回地址字符串指针,出错返回NULL(将数字(二进制网络地址)转换为23.24.54.4类型地址)
int inet_pton(int domain, const char *restrict str, void *restrict addr);
返回值:成功返回1,格式无效返回0,出错返回-1(pton=printable to numeric)(将23.3.4.5类型地址转换为数字,网络地址(二进制))
domain:地址族,只有AF_INET或AF_INET6。
对于inet_ntop(), size:保存文本缓存区(可打印的地址str)的大小。两个常数用于简化工作,INET_ADDRSTRLEN定义足够大的空间(可能最长的结构字符串)存放IPV4地址。INET6_ADDRSTRLEN定义用于存放IPv6的空间。
对于inet_pton(),str是一个null结尾的字符串,addr要足够大,IPv4至少32为,IPv6至少128位。
2.5将套接字与地址绑定
与客户端关联的套接字地址没有太大意义,可以让系统选择一个默认的地址。对于服务器需要给接收客户端请求的套接字绑定一个众所周知地址。客户端可以为服务器保留一个地址并在/etc/services或在某个名字服务(name service)中注册。
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *localaddress, socket_t len);
返回值:成功返回0,出错返回-1
sockaddr设置为通配符INADDR_ANY(IPv4),IN6ADDR_ANY(IPV6),这是套接字可绑定到所有的系统网络接口,意味着可以接收系统所有网卡的数据报。调用connect和listen时没有绑定到一个套接字,系统会选择一个地址绑定到套接字。
可以使用IN6ADDR_ANY_INIT把in6_addr结构初始化为通配符地址,但是这个常量只能用于声明中的“初始化器”。注意:inaddr_any是主机字节序列,在用作bind()的参数之前必须先转换为网络字节序列。in6addr_any和in6addr_any_init已经是网络字节序列。
当端口号为0提供给bind(),系统将为你选择未使用的本地端口。
2.6.获取套接字的关联地址
获取套接字关联的本地地址
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *localaddress, socket_t *addresslength);
返回值:成功放回0,出错返回-1
获取套接字关联的外部地址,套接字已经和对方连接,用于获取对方的地址
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *remoteaddress, socket_t *addresslength);
返回值:成功放回0,出错返回-1
sockfd: 想要获取其地址的套接字描述符。
remoteaddress和localaddress指向实现把地址信息存放在其中的地址结构,总会被强制转换为sockaddr*。若事先不知道IP协议版本,可以传入一个sockaddr_storage*接受结果。
addresslength:输入/输出型参数,是指向整形的指针(输入),整形指定sockaddr的大小,返回时(输出)被设置成返回地址的大小。若该地址与缓冲区长度不匹配,将其截断不报错。若没有绑定到套接字的地址,结果无意义。
2.7建立连接
处理面向连接的网络服务(SOCK_STREAM或SOCK_SEQPACKAGE),需要客户端套接字与服务区端套接字进行连接。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *foreignaddress, socklen_t addresslen);
返回值:成功返回0,出错返回-1
connect中的地址foreignaddress是想要与之通信的地址,如果套接字没有绑定到一个地址,connect会给调用者绑定一个默认接口。
addresslength指定地址结构的长度,通常给出sizeof(struct sockaddr_in)或sizeof(struct sockaddr_in6)。
2.8处理进入的连接
绑定后服务器套接字就具有一个地址(或至少一个套接字)。指示底层实现侦听来自套接字
的连接使用listen:
#include <sys/socket.h>
int listen(int socket, int queuelimit);
返回值:成功0,失败-1
queuelimit:任意时间等待进入连接数量的上限。实际值由系统指定,上线由<sys/socket.h>
中SOMAXCONN指定。一旦队列满了就拒绝处理连接的请求。
listen()导致内部状态改变为给定的套接字,使得将会处理进入的TCP连接请求。然后对
它们进行排队。
一旦把套接字配置为侦听,就可以开始接受其上客户的连接。首先,服务器现在似乎应该等待
它设置的套接字上的连接,通过套接字进行发送和接收,关闭它,然后重复这个过程,但是实际
上不是这样。已经被绑定到端口并且标记为“侦听”的套接字实际上从来不会用于发送和接收
。它被代之用作获取新套接字的方式,其中每个新套接字用于一条客户连接,服务器然后在
新套接字上执行发送和接收。
调用accept获取用于连接的套接字:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *clientaddress, socket_t *addresslength);
返回值:成功返回文件(套接字)描述符,失败返回-1
返回的套接字连接到connect的客户端。这个新的套接字描述符和原始套接字(sockfd)具有相同的套接字
类型和地址族。传给accept的原始套接字没有关联到这个连接,而是继续保持原来状态并接受其它连接
请求。
这个函数为套接字使队列的下一条连接出队,如果队列为空,就阻塞,直到下一个连接请求到达。
成功时就会利用连接到另一端的客户的地址和端口填充由clientaddr指定的结构的大小(即可用的空间
),一旦返回,就会包含反回的实际地址的大小。成功,返回连接到客户的新套接字的描述符。一旦失败
返回-1,大多数系统仅当传递了一个错误的套接字描述符时,accept才会失败。在一些平台,如果套接字
在创建后并在被接受前经历了网络级错误,那么它可能返回一个错误。
若不关心客户端标识,可以将参数addr和len设为NULL,否则在accept之前,应将参数clientaddr设为足够大的 缓冲区
来存放地址,并将addresslength设为设为代表这个缓冲区大小的整数的指针。返回时,accept会在缓冲区
填写客户端的地址并且更新指针addresslength所指向的整数为该地址大小。
如果没有连接请求,就阻塞到一个请求到来,如果sockfd是非阻塞模式,accept返回-1并将error设为
EAGAIN或EWOULDBLOCK(很多平台,EAGIN定义于EWOULDBLOCK相同)。
2.9 数据传输
在连接后,服务器和客户端的区别就消失了(至少socket api是这样)。连接的TCP套接字可以使用
#inlcude <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
返回值:返回发送的字节数,出错返回-1
发送数据。它与write类似,但是可以指定标志来改变处理传输数据的方式。使用send时套接字必须已经连接。
参数buf和nbytes和write中相同。
flags:
MSG_DONTROUTE:勿将数据路由出本地网络。
MSG_DONTWAIT: 允许非阻塞操作(等价于O_NONBLOCK)
MSG_EOR: 如果协议支持,此为记录结束。
MSG_OOB: 如果协议支持,发送外带数据。
send成功返回,并不必然表示连接另一端的进程接收数据,只表示数据已经无错误的发送到网络。
对于支持为报文设限的协议,如果单个报文超过协议所支持的最大尺寸,send失败并将error设为EMSGSIXZE,
对于字节流协议,send会默认会阻塞直到整个数据被传输。
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
返回值:以字节计数的消息长度,若无可用消息或对方已经按序结束则返回0,出错返回-1
函数recv与read类似,但允许指定选项控制如何接受。
flag
MSG_OOB: 如果协议支持,接受外带数据
MSG_PEEK: 返回报文内容而不真正取走报文
MSG_TRUNC: 即使报文被截断,要求返回的是报文的实际长度
MSG_WAITALL:等待直到所有的数据可用(仅SOCK_STREAM)
当指定MSG_PEEK标志时,可以查看写一个要读的数据而不真正取走,再次调用read或recv函数会返回
刚才查看的数据。
对于SOCK_STREAM套接字,接受的数据可以比请求的少,MSG_WAITALL阻止这种行为,除非需要的数据
全部接受recv才返回。对于sock_DGRAM和SOCK_SEQPACKET套接字,它不改变什么行为,因为基于报文的套接字
类型一次读取就返回整个报文。
如果发送者已经调用shutdown结束传输,或者网络协议支持默认的顺序关闭并且发送端已经关闭,那么
当所有的数据接受完毕后,recv返回0。
buf指向缓冲区,其中存放接收到的数据,len为缓冲区的长度,它是一次可以接受的最大字节数。recv默认
行为是阻塞到至少传输一些字节位置(在多数系统上,将导致recv的通用者解除阻塞的最小数据量是1字节)。
注意:TCP是一种字节流协议,不会保留send()边界。在接收者上调用recv()一次所读取的字节数不一定
由调用send()一次所写入的字节数确定。
2.10使用IPv6
IPv6程序涉及了IPv6的地址结构和常量,其它没有什么不同。
socket(AF_INTE6, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in6 servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin6_family = AF_INET6;
servaddr.sin6_addr = in6addr_any; //不需要转换为网络字节序列,已经是了。
servaddr.sin6_port = htons(servport);
IPv6的地址长度:INET6_ADDRSTRLE;