计算机网络学习笔记(二)- Socket编程
Socket API概述
- 标识通信端点(对外):IP地址+端口号
- 操作系统/进程如何管理套接字(对内):套接字描述符(创建了一个套接字,怎么找到并调用它,内部通过套接字描述符,外部通过IP地址+端口号,也就是通过IP地址和端口号即可定位外部的一个套接字)
Socket抽象
- 类似于文件的抽象
- 当应用进程创建套接字时,操作系统分配一个数据结构存储该套接字相关信息,返回套接字描述符
- 地址结构
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 API函数
WSAStartup函数
- 使用Socket的应用程序在使用Socket之前必须首先调用WSAStartup函数
- 两个参数:
• 第一个参数指明程序请求使用的WinSock版本,其中高位字节指明副版本、低位字节指明主版本,十六进制整数,例如0x102表示2.1版
• 第二个参数返回实际的WinSock的版本信息,指向WSADATA结构的指针
// 函数调用格式
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
// 调用示例(使用2.1版本的WinSock的程序代码段)
wVersionRequested = MAKEWORD( 2, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
WSACleanup函数
- 应用程序在完成对请求的Socket库的使用, 最后要调用WSACleanup函数
- 解除与Socket库的绑定
- 释放Socket库所占用的系统资源
// 调用格式
int WSACleanup (void);
Socket函数
- 创建套接字
- 操作系统返回套接字描述符(sd)
- 第一个参数(协议族): protofamily = PF_INET(TCP/IP)
- 第二个参数(套接字类型):
如下图所示,type参数有三种选择,SOCK_STREAM对应TCP类型,SOCKET_DGRAM对应UDP类型,SOCK_RAW直接对应网络层,当使用此方式时要求权限非常高,如果是Linux系统,则要求有root权限
- 第三个参数(协议号):0为默认
// 调用格式
sd = socket(protofamily, type, proto);
// 调用示例
struct protoent *p;
p = getprotobyname("tcp");
SOCKET sd = socket(PF_INET, SOCK_STREAM, p->p_proto);
Closesocket函数
- 关闭一个描述符为sd的套接字
- 如果多个进程共享一个套接字,调用closesocket将套接字引用计数减1,减至0才关闭
- 一个进程中的多线程对一个套接字的使用无计数
- 如果进程中的一个线程调用closesocket将一个套接字关闭,该进程中的其他线程也将不能访问该套接字
- 返回值:
• 0:成功
• SOCKET_ERROR:失败
// 调用格式
int closesocket(SOCKET sd);
bind函数
- 绑定套接字的本地端点地址:IP地址+端口号
- 参数:
• 套接字描述符(sd)
• 端点地址(localaddr):结构sockaddr_in
• 地址结构的长度 - 客户程序一般不必调用bind函数
- 当遇见如下图所示情形,即一个主机/服务器连接了两个网络,也就有了两个IP地址,此时采用地址通配符的方式,地址通配符:INADDR_ANY
int bind(sd, localaddr, addrlen);
listen函数
- 置服务器端的流套接字处于监听状态(监听状态是指网络服务端程序所处的一种状态,在该状态下,服务端程序等待客户端的请求)
• 仅服务器端调用
• 仅用于面向连接的流套接字 - 设置连接请求队列大小(queuesize)(客户端请求队列,当有来自客户端的请求时,放入队列,但不响应请求,响应请求时另一个函数)
- 返回值:
• 0:成功
• SOCKET_ERROR:失败
// 调用格式
int listen(sd, queuesize);
connect函数
- 客户程序调用connect函数来使客户套接字(sd)与特定计算机的特定端口(saddr)的套接字(服务)进行连接(用于TCP建立连接)
- 用于客户端
- 可用于TCP客户端也可以用于UDP客户端
• TCP客户端:建立TCP连接
• UDP客户端:指定服务器端点地址(UDP是一种无连接的方式,所以这个函数并不能有效建立UDP的连接)
accept函数
- 服务程序调用accept函数从处于监听状态的流套接字sd的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道
• 仅用于TCP套接字
• 仅用于服务器 - 利用新创建的套接字(newsock)与客户通信
// 调用格式
newsock = accept(sd, caddr, caddrlen);
send, sendto函数
- send函数用于TCP套接字(客户与服务器)或调用了connect函数的UDP客户端套接字(send函数用于发送数据,但函数并未指定发送的端口地址,这是因为TCP已经建立了连接,不需要指定,而调用了connect函数的UDP客户端,由于指定了端口,所以函数也不需要指定发送端口)
- sendto函数用于UDP服务器端套接字与未调用connect函数的UDP客户端套接字
// 调用格式
send(sd,*buf,len,flags);
sendto(sd,*buf,len,flags,destaddr,addrlen);
recv, recvfrom函数
- recv函数从TCP连接的另一端接收数据,或者从调用了connect函数的UDP客户端套接字接收服务器发来的数据(receive函数用于接受数据,但函数并未指定发送的端口地址,这是因为TCP已经建立了连接,不需要指定,而调用了connect函数的UDP客户端,由于指定了端口,所以函数也不需要指定发送端口)
- recvfrom函数用于从UDP服务器端套接字与未调用connect函数的UDP客户端套接字接收对端数据
// 调用格式
recv(sd,*buffer,len,flags);
recvfrom(sd,*buf,len,flags,senderaddr,saddrlen);
setsockopt, getsockopt
- setsockopt()函数用来设置套接字sd的选项参数
- getsockopt()函数用于获取任意类型、任意状态套接口的选项当前值,并把结果存入optval
// 调用格式
int setsockopt(int sd, int level, int optname, *optval, int optlen);
int getsockopt(int sd, int level, int optname, *optval, socklen_t *optlen);
网络字节顺序转换函数
- htons: 本地字节顺序→网络字节顺序(16bits)
- ntohs: 网络字节顺序→本地字节顺序(16bits)
- htonl: 本地字节顺序→网络字节顺序(32bits)
- ntohl: 网络字节顺序→本地字节顺序(32bits)
网络应用的Socket API(TCP)调用基本流程
“ * ”表示阻塞,就是这个函数不执行,就一直无法向下执行,一直等待该函数函数执行
客户端软件设计
解析服务器IP地址
- 客户端可能使用域名(如:study.163.com)或IP地址 (如:123.58.180.121)标识服务器
- IP协议需要使用32位二进制IP地址
- 需要将域名或IP地址转换为32位IP地址
• 函数inet_addr( ) 实现点分十进制IP地址到32位IP地址转换
• 函数gethostbyname( ) 实现域名到32位IP地址转换,返回一个指向结构hostent 的指针
struct hostent {
char FAR* h_name; /*official host name */
char FAR* FAR* h_aliases; /*other aliases */
short h_addrtype; /*address type */
short h_lengty; /*address length */
char FAR* FAR* h_addr_list; /*list of address */
};
#define h_addr h_addr_list[0]
解析服务器端口号
- 客户端还可能使用服务名(如HTTP)标识服务器端口
- 需要将服务名转换为熟知端口号
• 函数getservbyname( )
• 返回一个指向结构servent的指针
struct servent {
char FAR* s_name; /*official service name */
char FAR* FAR* s_aliases; /*other aliases */
short s_port; /*port for this service */
char FAR* s_proto; /*protocol to use */
};
解析协议号
- 客户端可能使用协议名(如:TCP)指定协议
- 需要将协议名转换为协议号(如:6)
• 函数getprotobyname ( ) 实现协议名到协议号的转换
• 返回一个指向结构protoent的指针
struct protoent {
char FAR* p_name; /*official protocol name */
char FAR* FAR* p_aliases; /*list of aliases allowed */
short p_proto; /*official protocol number*/
};
TCP软件客户端软件流程
- 确定服务器IP地址与端口号
- 创建套接字
- 分配本地端点地址(IP地址+端口号)
- 连接服务器(套接字)
- 遵循应用层协议进行通信
- 关闭/释放连接
- 设计一个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;
}
- 访问DAYTIME服务的客户端(TCP)
• 获取日期和时间
• 双协议服务(TCP、 UDP),端口号13
• TCP版利用TCP连接请求触发服务
• UDP版需要客户端发送一个请求
/* 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客户端软件设计流程
- 确定服务器IP地址与端口号
- 创建套接字
- 分配本地端点地址(IP地址+端口号)
- 指定服务器端点地址,构造UDP数据报
- 遵循应用层协议进行通信
- 关闭/释放套接字
服务器软件设计
循环无连接服务器基本流程
- 创建套接字
- 绑定端点地址(INADDR_ANY+端口号)
- 反复接收来自客户端的请求
- 遵循应用层协议,构造响应报文,发送给客户
注:
- 服务器端不能使用connect()函数
- 无连接服务器使用sendto()函数发送数据报
- 调用recvfrom()函数接收数据时,自动提取
循环面向连接服务器基本流程
- 创建(主)套接字,并绑定熟知端口号;
- 设置(主)套接字为被动监听模式,准备用于服务器;
- 调用accept()函数接收下一个连接请求(通过主套接字),创建新套接字用于与该客户建立连接;
- 遵循应用层协议,反复接收客户请求,构造并发送响应(通过新套接字);
- 完成为特定客户服务后,关闭与该客户之间的连接,返回步骤3.
并发无连接服务器基本流程
- 主线程1: 创建套接字,并绑定熟知端口号;
- 主线程2: 反复调用recvfrom()函数,接收下一个客户请求,并创建新线程处理该客户响应;
- 子线程1: 接收一个特定请求;
- 子线程2: 依据应用层协议构造响应报文,并调用sendto()发送;
- 子线程3: 退出(一个子线程处理一个请求后即终止)。
并发面向连接服务器基本流程
- 主线程1: 创建(主)套接字,并绑定熟知端口号;
- 主线程2: 设置(主)套接字为被动监听模式,准备用于服务器;
- 主线程3: 反复调用accept()函数接收下一个连接请求(通过主套接字),并创建一个新的子线程处理该客户响应;
- 子线程1: 接收一个客户的服务请求(通过新创建的套接字);
- 子线程2: 遵循应用层协议与特定客户进行交互;
- 子线程3: 关闭/释放连接并退出(线程终止)
/* 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) {
case 1:
break;
case 2:
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);
}