计算机网络学习笔记(二)- Socket编程

计算机网络学习笔记(二)- Socket编程

  • Socket API概述
    • Socket抽象
  • Socket API函数
    • WSAStartup函数
    • WSACleanup函数
    • Socket函数
    • Closesocket函数
    • bind函数
    • listen函数
    • connect函数
    • accept函数
    • send, sendto函数
    • recv, recvfrom函数
    • setsockopt, getsockopt
    • 网络字节顺序转换函数
    • 网络应用的Socket API(TCP)调用基本流程
  • 客户端软件设计
    • 解析服务器IP地址
    • 解析服务器端口号
    • 解析协议号
    • TCP软件客户端软件流程
    • UDP客户端软件设计流程
  • 服务器软件设计
    • 循环无连接服务器基本流程
    • 循环面向连接服务器基本流程
    • 并发无连接服务器基本流程
    • 并发面向连接服务器基本流程

Socket API概述

  1. 标识通信端点(对外):IP地址+端口号
  2. 操作系统/进程如何管理套接字(对内):套接字描述符(创建了一个套接字,怎么找到并调用它,内部通过套接字描述符,外部通过IP地址+端口号,也就是通过IP地址和端口号即可定位外部的一个套接字)

Socket抽象

  • 类似于文件的抽象
  • 当应用进程创建套接字时,操作系统分配一个数据结构存储该套接字相关信息,返回套接字描述符
    计算机网络学习笔记(二)- Socket编程_第1张图片
  • 地址结构
	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函数

  1. 使用Socket的应用程序在使用Socket之前必须首先调用WSAStartup函数
  2. 两个参数:
    • 第一个参数指明程序请求使用的WinSock版本,其中高位字节指明副版本、低位字节指明主版本,十六进制整数,例如0x102表示2.1版
    • 第二个参数返回实际的WinSock的版本信息,指向WSADATA结构的指针
	// 函数调用格式
	int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
	// 调用示例(使用2.1版本的WinSock的程序代码段)
	wVersionRequested = MAKEWORD( 2, 1 ); 
	err = WSAStartup( wVersionRequested, &wsaData );

WSACleanup函数

  1. 应用程序在完成对请求的Socket库的使用, 最后要调用WSACleanup函数
  2. 解除与Socket库的绑定
  3. 释放Socket库所占用的系统资源
	// 调用格式
	int WSACleanup (void);

Socket函数

  1. 创建套接字
  2. 操作系统返回套接字描述符(sd)
  3. 第一个参数(协议族): protofamily = PF_INET(TCP/IP)
  4. 第二个参数(套接字类型):
    如下图所示,type参数有三种选择,SOCK_STREAM对应TCP类型,SOCKET_DGRAM对应UDP类型,SOCK_RAW直接对应网络层,当使用此方式时要求权限非常高,如果是Linux系统,则要求有root权限
计算机网络学习笔记(二)- Socket编程_第2张图片
  1. 第三个参数(协议号):0为默认
	// 调用格式
	sd = socket(protofamily, type, proto);
	// 调用示例
	struct protoent *p;
	p = getprotobyname("tcp");
	SOCKET sd = socket(PF_INET, SOCK_STREAM, p->p_proto);

Closesocket函数

  1. 关闭一个描述符为sd的套接字
  2. 如果多个进程共享一个套接字,调用closesocket将套接字引用计数减1,减至0才关闭
  3. 一个进程中的多线程对一个套接字的使用无计数
  4. 如果进程中的一个线程调用closesocket将一个套接字关闭,该进程中的其他线程也将不能访问该套接字
  5. 返回值:
    • 0:成功
    • SOCKET_ERROR:失败
	// 调用格式
	int closesocket(SOCKET sd);

bind函数

  1. 绑定套接字的本地端点地址:IP地址+端口号
  2. 参数:
    • 套接字描述符(sd)
    • 端点地址(localaddr):结构sockaddr_in
    • 地址结构的长度
  3. 客户程序一般不必调用bind函数
  4. 当遇见如下图所示情形,即一个主机/服务器连接了两个网络,也就有了两个IP地址,此时采用地址通配符的方式,地址通配符:INADDR_ANY
	int bind(sd, localaddr, addrlen);

listen函数

  1. 置服务器端的流套接字处于监听状态(监听状态是指网络服务端程序所处的一种状态,在该状态下,服务端程序等待客户端的请求)
    仅服务器端调用
    仅用于面向连接的流套接字
  2. 设置连接请求队列大小(queuesize)(客户端请求队列,当有来自客户端的请求时,放入队列,但不响应请求,响应请求时另一个函数)
  3. 返回值:
    • 0:成功
    • SOCKET_ERROR:失败
	// 调用格式
	int listen(sd, queuesize);

connect函数

  1. 客户程序调用connect函数来使客户套接字(sd)与特定计算机的特定端口(saddr)的套接字(服务)进行连接(用于TCP建立连接)
  2. 用于客户端
  3. 可用于TCP客户端也可以用于UDP客户端
    • TCP客户端:建立TCP连接
    • UDP客户端:指定服务器端点地址(UDP是一种无连接的方式,所以这个函数并不能有效建立UDP的连接)

accept函数

  1. 服务程序调用accept函数从处于监听状态的流套接字sd的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道
    仅用于TCP套接字
    仅用于服务器
  2. 利用新创建的套接字(newsock)与客户通信
	// 调用格式
	newsock = accept(sd, caddr, caddrlen);

send, sendto函数

  1. send函数用于TCP套接字(客户与服务器)或调用了connect函数的UDP客户端套接字(send函数用于发送数据,但函数并未指定发送的端口地址,这是因为TCP已经建立了连接,不需要指定,而调用了connect函数的UDP客户端,由于指定了端口,所以函数也不需要指定发送端口)
  2. sendto函数用于UDP服务器端套接字与未调用connect函数的UDP客户端套接字
	// 调用格式
	send(sd,*buf,len,flags);
	sendto(sd,*buf,len,flags,destaddr,addrlen);

recv, recvfrom函数

  1. recv函数从TCP连接的另一端接收数据,或者从调用了connect函数的UDP客户端套接字接收服务器发来的数据(receive函数用于接受数据,但函数并未指定发送的端口地址,这是因为TCP已经建立了连接,不需要指定,而调用了connect函数的UDP客户端,由于指定了端口,所以函数也不需要指定发送端口)
  2. recvfrom函数用于从UDP服务器端套接字与未调用connect函数的UDP客户端套接字接收对端数据
	// 调用格式
	recv(sd,*buffer,len,flags);
	recvfrom(sd,*buf,len,flags,senderaddr,saddrlen);

setsockopt, getsockopt

  1. setsockopt()函数用来设置套接字sd的选项参数
  2. 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)调用基本流程

“ * ”表示阻塞,就是这个函数不执行,就一直无法向下执行,一直等待该函数函数执行

计算机网络学习笔记(二)- Socket编程_第3张图片

客户端软件设计

解析服务器IP地址

  1. 客户端可能使用域名(如:study.163.com)或IP地址 (如:123.58.180.121)标识服务器
  2. IP协议需要使用32位二进制IP地址
  3. 需要将域名或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]

解析服务器端口号

  1. 客户端还可能使用服务名(如HTTP)标识服务器端口
  2. 需要将服务名转换为熟知端口号
    • 函数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 */
	};

解析协议号

  1. 客户端可能使用协议名(如:TCP)指定协议
  2. 需要将协议名转换为协议号(如: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软件客户端软件流程

  1. 确定服务器IP地址与端口号
  2. 创建套接字
  3. 分配本地端点地址(IP地址+端口号)
  4. 连接服务器(套接字)
  5. 遵循应用层协议进行通信
  6. 关闭/释放连接
  • 设计一个connectsock过程封装底层代码
	/* consock.cpp - connectsock */
	#include 
	#include 
	#include 
	#include 
	#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 
	#include 
	#include 
	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客户端软件设计流程

  1. 确定服务器IP地址与端口号
  2. 创建套接字
  3. 分配本地端点地址(IP地址+端口号)
  4. 指定服务器端点地址,构造UDP数据报
  5. 遵循应用层协议进行通信
  6. 关闭/释放套接字

服务器软件设计

循环无连接服务器基本流程

  1. 创建套接字
  2. 绑定端点地址(INADDR_ANY+端口号)
  3. 反复接收来自客户端的请求
  4. 遵循应用层协议,构造响应报文,发送给客户

  1. 服务器端不能使用connect()函数
  2. 无连接服务器使用sendto()函数发送数据报
    计算机网络学习笔记(二)- Socket编程_第4张图片
  3. 调用recvfrom()函数接收数据时,自动提取
    计算机网络学习笔记(二)- Socket编程_第5张图片

循环面向连接服务器基本流程

  1. 创建(主)套接字,并绑定熟知端口号;
  2. 设置(主)套接字为被动监听模式,准备用于服务器;
  3. 调用accept()函数接收下一个连接请求(通过主套接字),创建新套接字用于与该客户建立连接;
  4. 遵循应用层协议,反复接收客户请求,构造并发送响应(通过新套接字);
  5. 完成为特定客户服务后,关闭与该客户之间的连接,返回步骤3.

并发无连接服务器基本流程

  1. 主线程1: 创建套接字,并绑定熟知端口号;
  2. 主线程2: 反复调用recvfrom()函数,接收下一个客户请求,并创建新线程处理该客户响应;
  3. 子线程1: 接收一个特定请求;
  4. 子线程2: 依据应用层协议构造响应报文,并调用sendto()发送;
  5. 子线程3: 退出(一个子线程处理一个请求后即终止)。

并发面向连接服务器基本流程

  1. 主线程1: 创建(主)套接字,并绑定熟知端口号;
  2. 主线程2: 设置(主)套接字为被动监听模式,准备用于服务器;
  3. 主线程3: 反复调用accept()函数接收下一个连接请求(通过主套接字),并创建一个新的子线程处理该客户响应;
  4. 子线程1: 接收一个客户的服务请求(通过新创建的套接字);
  5. 子线程2: 遵循应用层协议与特定客户进行交互;
  6. 子线程3: 关闭/释放连接并退出(线程终止)
	/* TCPdtd.cpp - main, TCPdaytimed */
	#include 
	#include 
	#include 
	#include 
	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);
	}

你可能感兴趣的:(计算机网络学习笔记(二)- Socket编程)