Socket编程
网络编程的综览
定义
Socket:应用进程与操作系统之间的API,实现了应用进程的控制权和操作系统的控制全进行转换的功能。
形象来讲,套接字提供了进程中通信的抽象机制,其在某种程度上就像下图中所示的结构那样。
应用层接口介绍
- Berkeley UNIX 操作系统定义了一种 API,称为 套接字接口(socket interface),简称套接字( socket)。
- 微软公司在其操作系统中采用了套接字接口 API ,形成了一个稍有不同的 API,并称之为 Windows Socket Interface,WINSOCK。
- AT&T 为其 UNIX 系统 V 定义了一种 API,简写 为 TLI (Transport Layer Interface)
套接字的寻址
- 标识通信端点(对外): IP地址+端口号
- 操作系统/进程如何管理套接字(对内): 套接字描述符(socket descriptor):小整数,表示一个套接字名称
当应用进程创建套接字时,操作系统分配一个数据结构存储该套接字相关信息,返回套接字描述符
套接字的地址接口:sockaddr_in
struct sockaddr_in { u_char sin_len; /*地址长度 */ u_char sin_family; /*地址族(TCP/IP:AF_INET) */ u_short sin_port; /*端口号 */ struct in_addr sin_addr; /*IP地址 */ char sin_zero[8]; /*未用(置0) */ }
Socket函数工作流程
Socket API 函数(WinSocket)
库使用函数
WSAStartup
函数形式
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
函数说明
socket编程要调用各种socket函数,需要库Ws2_32.lib和头文件Winsock2.h,这里的WSAStartup就是为了向操作系统说明,我们要用哪个库文件,让该库文件与当前的应用程序绑定,从而就可以调用该版本的socket的各种函数了。
参数说明:
wVersionRequested指明程序请求使用的WinSock版本,其中高位字节指明副版本、低位字节指明主版本. 采用十六进制整数,例如0x102表示2.1版
lpWSAData返回实际的WinSock的版本信息,指向WSADATA结构的指针
当正确初始化时,WSAStartup会返回0。失败将返回错误代码。
但在WSAStartup函数的第一个参数中胡乱设置了一个版本号,WSAStartup仍然会返回0。最后经测试发现,如果在WSAStartup函数第一个参数中设置的版本号不存在,那么会自动使用WinSock库中最低的版本1.1。
Eg: 使用2.1版本的WinSock的程序代码段
wVersionRequested = MAKEWORD( 2, 1 ); err = WSAStartup( wVersionRequested, &wsaData );
WORD数据结构说明:
#define MAKEWORD(a,b) ((WORD) (((BYTE) (a)) | ((WORD) ((BYTE) (b))) << 8))
eg:
比如a=2;b=1. 2的二进制是00000010 1的二进制为00000001 B是表示高8位,A表示低8位 合并起来就是100000010
WSACleanup
函数形式
int WSACleanup (void);
函数说明
应用程序在完成对请求的Socket库的使用,最后要调用WSACleanup函数,解除与Socket库的绑定,释放Socket库所占用的系统资源
参数说明
- 操作成功返回值为0;否则返回值为SOCKET_ERROR
socket创建与关闭函数
socket
函数形式
SOCKET sd = socket(int af, int type, int protocol);
函数说明
创建套接字,并且返回套接字描述符(sd)
参数说明
- af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
也可以使用 PF 前缀,PF 是“Protocol Family”的简写,它和 AF 是一样的。例如,PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6。
- type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)以及 SOCK_RAW(TCP/IP)
- protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。而如果只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议。
- 返回套接字的描述符
Eg:
创建一个流套接字的代码
struct protoent *p; p = getprotobyname("tcp"); SOCKET sd=socket(PF_INET,SOCK_STREAM,p->p_proto);
套接字类型的说明:
- TCP:SOCK_STREAM
- UDP:SOCK_DGRAM
- 网络层的某些协议(IP/ICMP/IGMP):SOCK_RAM
Closesocket
函数形式
int closesocket(SOCKET sd);
函数说明
- 关闭一个描述符为sd的套接字
- 如果多个进程共享一个套接字,调用closesocket 将套接字引用计数减1,减至0才关闭
- 一个进程中的多线程对一个套接字的使用无计数 如果进程中的一个线程调用closesocket将一个套接字 关闭,该进程中的其他线程也将不能访问该套接字
- 如无错误发生,则closesocket()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
bind
函数形式
int bind(SOCKET sd,const struct sockaddr *addr,int addrlen);
函数说明
- sd表示绑定套接字的本地端点地址,即绑定本机的某个进程的地址
- 客户端一般不需要调用bind函数,操作系统为套接字进行绑定本地端点地址
- 服务器端需要调用bind函数,绑定熟知端口号(使用某一网络协议常用的端口号)
参数说明
- 套接字描述符(sd)
- addr 是一个指向sockaddr结构体类型的指针
- 参数addrlen表示addr结构的长度,可以用sizeof操作符获得。
- 如无错误发生,则bind()返回0。否则的话,将返回SOCKET_ERROR,应用程序可通过WSAGetLastError()获取相应错误代码。
sockaddr结构体说明
sockaddr_in结构体说明
struct sockaddr_in{ sa_family_t sin_family; //地址族(Address Family),也就是地址类型 uint16_t sin_port; //16位的端口号 struct in_addr sin_addr; //32位IP地址 char sin_zero[8]; //不使用,一般用0填充 };
- sin_family 和 socket() 的第一个参数的含义相同,取值也要保持一致。
- sin_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号
- sin_addr 是 struct in_addr 结构体类型的变量。端口号需要用 htons() 函数转换(也就将本地编码转换为网络编码)。
- sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。上面的代码中,先用 memset() 将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 自然就是 0 了。
struct in_addr结构体说明
struct in_addr{ in_addr_t s_addr; //32位的IP地址 };
n_addr_t 在头文件 <netinet/in.h> 中定义,等价于 unsigned long,长度为4个字节。也就是说,s_addr 是一个整数,而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换,例如:
unsigned long ip = inet_addr("127.0.0.1"); printf("%ld\n", ip);
运行结果:16777343
为什么要搞这么复杂,结构体中嵌套结构体,而不用 sockaddr_in 的一个成员变量来指明IP地址呢?socket() 函数的第一个参数已经指明了地址类型,为什么在 sockaddr_in 结构体中还要再说明一次呢,这不是啰嗦吗?这些繁琐的细节确实给初学者带来了一定的障碍,我想,这或许是历史原因吧,后面的接口总要兼容前面的代码。各位读者一定要有耐心,暂时不理解没有关系,根据教程中的代码“照猫画虎”即可,时间久了自然会接受。
为什么使用 sockaddr_in 而不使用 sockaddr
bind() 第二个参数的类型为 sockaddr,而代码中却使用 sockaddr_in,然后再强制转换为 sockaddr,这是为什么呢?
struct sockaddr{ sa_family_t sin_family; //地址族(Address Family),也就是地址类型 char sa_data[14]; //IP地址和端口号 };
sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。
可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址,它的定义如下:
struct sockaddr_in6 { sa_family_t sin6_family; //(2)地址类型,取值为AF_INET6 in_port_t sin6_port; //(2)16位端口号 uint32_t sin6_flowinfo; //(4)IPv6流信息 struct in6_addr sin6_addr; //(4)具体的IPv6地址 uint32_t sin6_scope_id; //(4)接口范围ID };
关于INADDR _ANY的说明
转载自:INADDR_ANY的确切含义
问:
很多书上都说“将sin_addr设置为INADDR_ANY,则表示所有的IP地址,也即所有的计算机”,这样的解说让人费解。
答:
INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。
当服务器的监听地址是INADDR_ANY时,意思不是监听所有的客户端IP。而是服务器端的IP地址可以随意配置,这样使得该服务器端程序可以运行在任意计算机上,可使任意计算机作为服务器,便于程序移植。将INADDR_ANY换成127.0.0.1也可以达到同样的目的。这样,当作为服务器的计算机的IP有变动或者网卡数量有增减,服务器端程序都能够正常监听来自客户端的请求。我是这么理解的。
比如一台电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某个端口,那他要监听哪个网卡地址的端口呢?如果绑定某个具体的ip地址,你只能监听你所设置的ip地址所在的网卡的端口,其它两块网卡无法监听端口,如果我需要三个网卡都监听,那就需要绑定3个ip,也就等于需要管理3个套接字进行数据交换,这样岂不是很繁琐?所以出现INADDR_ANY,你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。
connect
函数形式
int connect(SOCKET sockfd, const struct sockaddr *serv_addr, int addrlen);
参数说明
同bind函数,不再赘述
函数说明
- 客户端程序调用connect来使得客户端套接字(sd)与特定计算机的特定端口的套接字进行连接。
- 如果TCP连接,需要构建TCP连接
- 如果是UDP连接,则仅仅指定服务器端点地址。
listen
函数形式
int listen(SOCKET sd, int backlog);
函数说明
只用于服务端。
没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。(被动监听)
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。
当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误,对于 Windows,客户端会收到 WSAECONNREFUSED 错误。
如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。
参数说明
- sd 表示所要进行操作的服务器的套接字
- backlog 为请求队列的最大长度。
- 无错误,返回0,否则,返回SOCKET ERROR,windows上可以调用函数WSAGetLastError取得错误代码,在Linux可使用errno。
accept
函数形式
SOCKET accept(SOCKET sd, struct sockaddr *addr, int addrlen);
参数说明
- 同bind函数的参数说明。
- 返回一个新的套接字,否则返回NVALID_SOCKET
函数说明
- 服务程序调用accept函数从处于监听状态的流套接字sd的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道
- 仅用于TCP套接字
- 仅用于服务器
- 利用新创建的套接字(newsock)与客户通信
- 阻塞作用
send
函数形式
int send( SOCKET s, const char FAR* buf, int len, int flags);
参数说明
- s指明:已建立好连接的socket
- buf指明:存放欲发送的数据内容的缓存区地址
- len指明:实际要发送的数据的字节数
- flags 一般设0, 其他数值定义如下:
- MSG_OOB 传送的数据以out-of-band 送出.
- MSG_DONTROUTE 取消路由表查询
- MSG_DONTWAIT 设置为不可阻断运作
- MSG_NOSIGNAL 此动作不愿被SIGPIPE 信号中断.
- 返回值:成功则返回实际传送出去的字符数, 失败返回-1。
函数说明
- 不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。
- 客户程序一般用send函数向服务器发送请求,
- 而服务器则通常用send函数来向客户程序发送应答。
sendto
函数形式
int sendto(Socket s, const void *msg, int len, unsigned int flags, const struct sockaddr * to, int tolen);
参数说明
- 参数s为已建好连线的socket
- 如果利用UDP协议则不需经过连线操作. 参数msg 指向欲连线的数据内容
- 参数len指出缓存中数据的字节数
- 参数flags为标志位,一般设0, 详细描述请参考send().
- 参数to用来指定欲传送的网络地址, 结构sockaddr请参考bind().
- 参数tolen 为sockaddr的地址长度
函数说明
- 使用UDP发送数据的函数
recv
函数形式
int recv(SOCKET sock, char *buf, int len, int flags);
参数说明
同send
函数说明
- 在客户端或服务端接收数据使用 recv() 函数
recvfrom
函数形式
int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from,int *fromlen);
参数说明
- 参数同sendto
函数说明
- 使用UDP连接接受发送数据的函数
- 作为服务端可以进行捕获客户端的地址
setsockopt
函数形式
int setsockopt(Socket sd, int level, int optname, *optval, int optlen);
参数说明
- sd 表明需要设置状态的套接字
- level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
- 参数optname 代表欲设置的选项。
- 参数 optval 代表欲设置的值。
- 若无错误发生,setsockopt()返回0。否则的话,返回SOCKET_ERROR错误
函数说明
- 设定套接字状态
getsocket
函数形式
int getsockopt(Socket s, int level, int optname, void* optval, socklen_t* optlen);
参数说明
- 参数optlen 则为该空间的大小
- 其他均同setsockopt,只不过是进行赋值。
- 若无错误发生,setsockopt()返回0。否则的话,返回SOCKET_ERROR错误
函数说明
- getsockopt()会将sd 所指定的socket 状态返回.
网络字节顺序与本地字节顺序转换函数
- TCP/IP定义了标准的用于协议头中的二进制整数表示:网络字节顺序(network byte order)
- 某些Socket API函数的参数需要存储为网络字节顺序(如IP地址、端口号等)
- 可以实现本地字节顺序与网络字节顺序间转换的函数:
- htons: 本地字节顺序→网络字节顺序(16bits)
- ntohs: 网络字节顺序→本地字节顺序(16bits)
- htonl: 本地字节顺序→网络字节顺序(32bits)
- ntohl: 网络字节顺序→本地字节顺序(32bits)
IP地址转换函数
作用以及原因:
IP协议需要使用32位二进制IP地址,而一般所使用的地址为域名(study.163.com)或者点分十进制IP地址(123.58.180.121)进行标识,这就产生了转换问题。
- inet_addr( )实现从点分十进制IP地址到32位IP地址转换
unsigned long int inet_addr(const char *hostname);
- gethostbyname( )实现从域名到32位IP地址的转换
struct hostent *gethostbyname(const char *hostname);
对于hostent结构体的说明
struct hostent{ char *h_name; //official name char **h_aliases; //alias list int h_addrtype; //host address type int h_length; //address lenght char **h_addr_list; //address list }
可以依凭此图进行理解该结构体
服务名与端口号转换函数
getservbyname( )
struct servent * getservbyname(const char * name, const char *protoname);
函数说明
- 将服务名转换为熟知端口号
参数说明
- name:服务名
- protoname:协议
servent结构体说明
struct servent{ char* s_name; 服务名 char** s_aliases; 别名列表 int s_port; 端口号(网络字节序) char* s_proto; 使用的协议 }
协议名到协议号的转换函数
getprotobyname ( )
struct protoent *getprotobyname(const char * name);
函数说明
- 将协议名转换为协议号
protoent结构体说明
struct protoent { char *p_name; /*official protocol name */ char **p_aliases; /*list of aliases allowed */ short p_proto; /*official protocol number*/ };
客户端实现
TCP客户端工作流程
- 确定服务器IP地址与端口号
- 创建套接字
- 分配本地端点地址(IP地址+端口号)
- 连接服务器(套接字)
- 遵循应用层协议进行通信
- 关闭/释放连接
UDP客户端软件流程
- 确定服务器IP地址与端口号
- 创建套接字
- 分配本地端点地址(IP地址+端口号)
- 指定服务器端点地址,构造UDP数据报
- 遵循应用层协议进行通信
- 关闭/释放套接字
需要说明的细节:
- 在客户端,并不需要为创建的套接字绑定本地端点地址(IP地址与端口号),操作系统会自动为创建的套接字进行绑定。
- 对于TCP服务而言,一旦connect函数创建完成,建立TCP连接,对于简单的消息传输,客户端可以不向服务端发送数据。但是,对于UDP而言,必须向服务端发送数据,否则,服务端并不会直到已经建立连接。(这一点也可以从下面的例子中窥探一二)
底层函数connectsock
/* consock.cpp - connectsock */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <winsock.h>
#ifndef INADDR_NONE
#define INADDR_NONE 0xffffffff
#endif /* INADDR_NONE */
void errexit(const char *, ...);
/*-------------------------------------------------------
* connectsock - allocate & connect a socket using TCP or UDP
*------------------------------------------------------
*/
SOCKET connectsock(const char *host, const char *service, const char
*transport )
{
struct hostent *phe; /* pointer to host information entry */
struct servent *pse; /* pointer to service information entry */
struct protoent *ppe; /* pointer to protocol information entry */
struct sockaddr_in sin;/* an Internet endpoint address */
int s, type; /* socket descriptor and socket type */
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
/* Map service name to port number */
if ( pse = getservbyname(service, transport) )
sin.sin_port = pse->s_port;
else if ( (sin.sin_port = htons((u_short)atoi(service))) == 0 )
errexit("can't get \"%s\" service entry\n", service);
/* Map host name to IP address, allowing for dotted decimal */
if ( phe = gethostbyname(host) )
memcpy(&sin.sin_addr, phe->h_addr, phe->h_length);
else if ( (sin.sin_addr.s_addr = inet_addr(host))==INADDR_NONE)
errexit("can't get \"%s\" host entry\n", host);
/* Map protocol name to protocol number */
if ( (ppe = getprotobyname(transport)) == 0)
errexit("can't get \"%s\" protocol entry\n", transport);
/* Use protocol to choose a socket type */
if (strcmp(transport, "udp") == 0)
type = SOCK_DGRAM;
else
type = SOCK_STREAM;
/* Allocate a socket */
s = socket(PF_INET, type, ppe->p_proto);
if (s == INVALID_SOCKET)
errexit("can't create socket: %d\n", GetLastError());
/* Connect the socket */
if (connect(s, (struct sockaddr *)&sin, sizeof(sin))==SOCKET_ERROR)
errexit("can't connect to %s.%s: %d\n", host, service,
GetLastError());
return s;
}
具体客户端代码:使用TCP连接访问DAYTIME服务
/* TCPdtc.cpp - main, TCPdaytime */
#include <stdlib.h>
#include <stdio.h>
#include <winsock.h>
void TCPdaytime(const char *, const char *);
void errexit(const char *, ...);
SOCKET connectTCP(const char *, const char *);
#define LINELEN 128
#define WSVERS MAKEWORD(2, 0)
/*--------------------------------------------------------
* main - TCP client for DAYTIME service
*--------------------------------------------------------
*/
int main(int argc, char *argv[])
{
char *host = "localhost"; /* host to use if none supplied */
char *service = "daytime"; /* default service port */
WSADATA wsadata;
switch (argc) {
case 1:
host = "localhost";
break;
case 3:
service = argv[2];
/* FALL THROUGH */
case 2:
host = argv[1];
break;
default:
fprintf(stderr, "usage: TCPdaytime [host [port]]\n");
exit(1);
}
if (WSAStartup(WSVERS, &wsadata) != 0)
errexit("WSAStartup failed\n");
TCPdaytime(host, service);
WSACleanup();
return 0; /* exit */
}
/*-----------------------------------------------------
* TCPdaytime - invoke Daytime on specified host and print results
*-----------------------------------------------------
*/
void TCPdaytime(const char *host, const char *service)
{
char buf[LINELEN+1]; /* buffer for one line of text */
SOCKET s; /* socket descriptor */
int cc; /* recv character count */
s = connectTCP(host, service);
cc = recv(s, buf, LINELEN, 0);
while( cc != SOCKET_ERROR && cc > 0)
{
buf[cc] = '\0'; /* ensure null-termination */
(void) fputs(buf, stdout);
cc = recv(s, buf, LINELEN, 0);
}
closesocket(s);
}
具体客户端代码:使用UDP连接访问DAYTIME服务
/* UDPdtc.cpp - main, UDPdaytime */
#include <stdlib.h>
#include <stdio.h>
#include <winsock.h>
void UDPdaytime(const char *, const char *);
void errexit(const char *, ...);
SOCKET connectUDP(const char *, const char *);
#define LINELEN 128
#define WSVERS MAKEWORD(2, 0)
#define MSG “what daytime is it?\n"
/*--------------------------------------------------------
* main - UDP client for DAYTIME service
*--------------------------------------------------------
*/
int main(int argc, char *argv[])
{
char *host = "localhost"; /* host to use if none supplied */
char *service = "daytime"; /* default service port */
WSADATA wsadata;
switch (argc) {
case 1:
host = "localhost";
break;
case 3:
service = argv[2];
/* FALL THROUGH */
case 2:
host = argv[1];
break;
default:
fprintf(stderr, "usage: UDPdaytime [host [port]]\n");
exit(1);
}
if (WSAStartup(WSVERS, &wsadata) != 0)
errexit("WSAStartup failed\n");
UDPdaytime(host, service);
WSACleanup();
return 0; /* exit */
}
/*-----------------------------------------------------
* UDPdaytime - invoke Daytime on specified host and print results
*-----------------------------------------------------
*/
void UDPdaytime(const char *host, const char *service)
{
char buf[LINELEN+1]; /* buffer for one line of text */
SOCKET s; /* socket descriptor */
int n; /* recv character count */
s = connectUDP(host, service);
(void) send(s, MSG, strlen(MSG), 0);
/* Read the daytime */
n = recv(s, buf, LINELEN, 0);
if (n == SOCKET_ERROR)
errexit("recv failed: recv() error %d\n", GetLastError());
else
{
buf[cc] = '\0'; /* ensure null-termination */
(void) fputs(buf, stdout);
}
closesocket(s);
return 0; /* exit */
}
服务端实现
- 循环:一次只处理一个客户端的服务请求,当处理完此客户端的服务请求后,再处理其他的客户端的服务请求。即顺序处理客户端请求。
- 并发:同时处理多个服务器的请求
- 连接:TCP为传输协议
- 无连接:UDP为传输协议
循环无连接服务器
流程:
- 创建套接字
- 绑定端点地址(INADDR_ANY+端口号)
- 反复接受来自客户端的请求
- 遵循应用层协议,构造响应报文,发送给 客户
循环面向连接服务器服务器
流程
- 创建(主)套接字,并绑定熟知端口号;
- 设置(主)套接字为被动监听模式,准备用于 服务器;
- 调用accept()函数接收下一个连接请求(通过 主套接字),创建新套接字用于与该客户建立 连接;
- 遵循应用层协议,反复接收客户请求,构造并 发送响应(通过新套接字);
- 完成为特定客户服务后,关闭与该客户之间的 连接,返回步骤3.
并发无连接服务器基本流程
流程
主线程
- 创建套接字,并绑定熟知端口号;
- 反复调用recvfrom()函数,接收下一个客户请求,并创建新线程处理该客户响应;
子线程
- 接收一个特定请求
- 依据应用层协议构造响应报文,并调用 sendto()发送
- 退出(一个子线程处理一个请求后即终止)。
并发面向连接服务器基本流程
流程
主线程
- 创建套接字,并绑定熟知端口号;
- 设置(主)套接字为被动监听模式,准 备用于服务器;
- 反复调用accept()函数接收下一个连接 请求(通过主套接字),并创建一个新 的子线程处理该客户响应;
子线程
- 接收一个客户的服务请求(通过新创建的套接字)
- 遵循应用层协议与特定客户进行交互
- 关闭/释放连接并退出(线程终止)
具体代码示例
底层函数:passivesock
/* passsock.cpp - passivesock */
#include <stdlib.h>
#include <string.h>
#include <winsock.h>
void errexit(const char *, ...);
/*-----------------------------------------------------------------------
* passivesock - allocate & bind a server socket using TCP or UDP
*------------------------------------------------------------------------
*/
SOCKET passivesock(const char *service, const char *transport, int qlen)
{
struct servent *pse; /* pointer to service information entry */
struct protoent *ppe; /* pointer to protocol information entry */
struct sockaddr_in sin;/* an Internet endpoint address */
SOCKET s; /* socket descriptor */
int type; /* socket type (SOCK_STREAM, SOCK_DGRAM)*/
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
/* Map service name to port number */
if ( pse = getservbyname(service, transport) )
sin.sin_port = (u_short)pse->s_port;
else if ( (sin.sin_port = htons((u_short)atoi(service))) == 0 )
errexit("can't get \"%s\" service entry\n", service);
/* Map protocol name to protocol number */
if ( (ppe = getprotobyname(transport)) == 0)
errexit("can't get \"%s\" protocol entry\n", transport);
/* Use protocol to choose a socket type */
if (strcmp(transport, "udp") == 0)
type = SOCK_DGRAM;
else
type = SOCK_STREAM;
/* Allocate a socket */
s = socket(PF_INET, type, ppe->p_proto);
if (s == INVALID_SOCKET)
errexit("can't create socket: %d\n", GetLastError());
/* Bind the socket */
if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) == SOCKET_ERROR)
errexit("can't bind to %s port: %d\n", service,
GetLastError());
if (type == SOCK_STREAM && listen(s, qlen) == SOCKET_ERROR)
errexit("can't listen on %s port: %d\n", service,
GetLastError());
return s;}
/* passUDP.cpp - passiveUDP */
#include <winsock.h>
SOCKET passivesock(const char *, const char *, int);
/*-------------------------------------------------------------------------------------
* passiveUDP - create a passive socket for use in a UDP server
*-------------------------------------------------------------------------------------
*/
SOCKET passiveUDP(const char *service)
{
return passivesock(service, "udp", 0);
}
/* passTCP.cpp - passiveTCP */
#include <winsock.h>
SOCKET passivesock(const char *, const char *, int);
/*------------------------------------------------------------------------------------
* passiveTCP - create a passive socket for use in a TCP server
*------------------------------------------------------------------------------------
*/
SOCKET passiveTCP(const char *service, int qlen)
{
return passivesock(service, "tcp", qlen);
}
具体客户端代码:无连接循环DAYTIME服务器
/* UDPdtd.cpp - main, UDPdaytimed */
#include <stdlib.h>
#include <winsock.h>
#include <time.h>
void errexit(const char *, ...);
SOCKET passiveUDP(const char *);
#define WSVERS MAKEWORD(2, 0)
/*------------------------------------------------------------------------
* main - Iterative UDP server for DAYTIME service
*------------------------------------------------------------------------
*/
void main(int argc, char *argv[])
{
struct sockaddr_in fsin; /* the from address of a client */
char *service = "daytime"; /* service name or port number */
SOCKET sock; /* socket */
int alen; /* from-address length */
char * pts; /* pointer to time string */
time_t now; /* current time */
WSADATA wsadata;
switch (argc)
{
case 1:
break;
case 2:
service = argv[1];
break;
default:
errexit("usage: UDPdaytimed [port]\n");
}
if (WSAStartup(WSVERS, &wsadata) != 0)
errexit("WSAStartup failed\n");
sock = passiveUDP(service);
while (1)
{
alen = sizeof(struct sockaddr);
if (recvfrom(sock, buf, sizeof(buf), 0,
(struct sockaddr *)&fsin, &alen) == SOCKET_ERROR)
errexit("recvfrom: error %d\n", GetLastError());
(void) time(&now);
pts = ctime(&now);
(void) sendto(sock, pts, strlen(pts), 0,
(struct sockaddr *)&fsin, sizeof(fsin));
}
return 1; /* not reached */
}
面向连接并发DAYTIME服务器
/* TCPdtd.cpp - main, TCPdaytimed */
#include <stdlib.h>
#include <winsock.h>
#include <process.h>
#include <time.h>
void errexit(const char *, ...);
void TCPdaytimed(SOCKET);
SOCKET passiveTCP(const char *, int);
#define QLEN 5
#define WSVERS MAKEWORD(2, 0)
/*------------------------------------------------------------------------
* main - Concurrent TCP server for DAYTIME service
*------------------------------------------------------------------------
*/
void main(int argc, char *argv[])
{
struct sockaddr_in fsin; /* the from address of a client */
char *service = "daytime"; /* service name or port number*/
SOCKET msock, ssock; /* master & slave sockets */
int alen; /* from-address length */
WSADATA wsadata;
switch (argc) {
case1:
break;
case2:
service = argv[1];
break;
default:
errexit("usage: TCPdaytimed [port]\n");
}
if (WSAStartup(WSVERS, &wsadata) != 0)
errexit("WSAStartup failed\n");
msock = passiveTCP(service, QLEN);
while (1) {
alen = sizeof(struct sockaddr);
ssock = accept(msock, (struct sockaddr *)&fsin, &alen);
if (ssock == INVALID_SOCKET)
errexit("accept failed: error number %d\n",
GetLastError());
if (_beginthread((void (*)(void *)) TCPdaytimed, 0,
(void *)ssock) < 0) {
errexit("_beginthread: %s\n", strerror(errno));
}
}
return 1; /* not reached */
}
/*----------------------------------------------------------------------
* TCPdaytimed - do TCP DAYTIME protocol
*-----------------------------------------------------------------------
*/
void TCPdaytimed(SOCKET fd)
{
char * pts; /* pointer to time string */
time_t now; /* current time */
(void) time(&now);
pts = ctime(&now);
(void) send(fd, pts, strlen(pts), 0);
(void) closesocket(fd);
}