Socket编程

网络编程的综览

image-20200927072043789

定义

Socket:应用进程与操作系统之间的API,实现了应用进程的控制权和操作系统的控制全进行转换的功能。

形象来讲,套接字提供了进程中通信的抽象机制,其在某种程度上就像下图中所示的结构那样。

image-20200927073833341

应用层接口介绍

  • Berkeley UNIX 操作系统定义了一种 API,称为 套接字接口(socket interface),简称套接字( socket)
  • 微软公司在其操作系统中采用了套接字接口 API ,形成了一个稍有不同的 API,并称之为 Windows Socket Interface,WINSOCK。
  • AT&T 为其 UNIX 系统 V 定义了一种 API,简写 为 TLI (Transport Layer Interface)

套接字的寻址

  • 标识通信端点(对外): IP地址+端口号
  • 操作系统/进程如何管理套接字(对内): 套接字描述符(socket descriptor):小整数,表示一个套接字名称

当应用进程创建套接字时,操作系统分配一个数据结构存储该套接字相关信息,返回套接字描述符

image-20200927074617633

套接字的地址接口: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函数工作流程

image-20200927214859619

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

image-20200927082727276

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
}

可以依凭此图进行理解该结构体

image-20201001103252785

服务名与端口号转换函数

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);
}
posted @ 2020-10-13 19:21  zqybegin  阅读(224)  评论(0编辑  收藏  举报