Socket网络编程(二):主要API调用方法

10 篇文章 0 订阅
5 篇文章 0 订阅

本文参考了http://c.biancheng.net/view/2123.html

Socket主要API调用方法

windows下socket的API和linux下的API大致相同,只是在某些细节上有些细微的差别。


包含头文件和初始化

Linux socket常用头文件

<sys/socket.h> //与套接字相关的函数声明和结构体定义,如socket()、bind()、connect()及struct sockaddr的定义等
<sys/types.h> //primitive system data types(包含很多类型重定义,如pid_t、int8_t等)
<netinet/in.h> //某些结构体声明、宏定义,如struct sockaddr_in、PROTO_ICMP、INADDR_ANY等
<arpa/inet.h> //某些函数声明,如inet_ntop()、inet_ntoa()等
<sys/ioctl.h> //I/O控制操作相关的函数声明,如ioctl()
<stdlib.h> //某些结构体定义和宏定义,如EXIT_FAILURE、EXIT_SUCCESS等
<netdb.h> //某些结构体定义、宏定义和函数声明,如struct hostent、struct servent、gethostbyname()、gethostbyaddr()、herror()等

windows 头文件及初始化

WinSock(Windows Socket)编程依赖于系统提供的动态链接库(DLL),有两个版本:

  • 较早的DLL是 wsock32.dll,大小为 28KB,对应的头文件为 winsock1.h;
  • 最新的DLL是 ws2_32.dll,大小为 69KB,对应的头文件为 winsock2.h。

使用 DLL 之前必须把 DLL 加载到当前程序,你可以在编译时加载,也可以在程序运行时加载,这里使用#pragma命令,在编译时加载;

#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")

在函数主体中使用相关API之前需要进行初始化;

	WSAData wsaData;  
    if(0 != WSAStartup(MAKEWORD(2,2),&wsaData))  
        printf("初始化失败!%d\n",WSAGetLastError());    

socket()

socket()是用来创建套接字的,Linux中socket函数原型为:

int socket(int af, int type, int protocol);
  1. 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。
    大家需要记住127.0.0.1,它是一个特殊IP地址,表示本机地址,后面的教程会经常用到。
    你也可以使用 PF 前缀,PF 是“Protocol Family”的简写,它和 AF 是一样的。例如,PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6。
  2. type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字),我们已经在《套接字有哪些类型》一节中进行了介绍。
  3. protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

linux系统提供的socket()函数返回一个int整型,这个整型也就是一个FD文件描述符;

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP协议

上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议:

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字

windows系统提供的socket函数参数和用法相同,只是返回值是windows系统定义的SOCKET类型的句柄。

SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字

bind()和connect()

bind()函数原型

bind()函数的功能是将一个套接字socket与特定的IP地址和端口绑定。

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows

sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出,返回值为一个int整型,可以根据返回值判断是否绑定成功。

下面给出一个示例,如何将套接字绑定到一个IP地址和端口:

//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
serv_addr.sin_family = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
serv_addr.sin_port = htons(1234);  //端口

//将套接字和IP、端口绑定 
 if(SOCKET_ERROR ==bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)))
	 printf("bind failed!%d\n",WSAGetLastError());  

可以看到这里定义了一个sockaddr_in结构体来保存协议族、IP地址以及端口号,我们可以看一下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填充
};

至于为什么要使用 sockaddr_in 结构体,然后再强制转换为 sockaddr 类型,其实这是一个历史遗留问题,后来的接口总是得考虑兼容之前的代码,所以这样的操作看似繁琐,却很有必要。其实可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。

另外绑定IP地址和绑定端口的时候有时候会用到htonl() 函数和htons() 函数,这个涉及到网络数据大小端的问题:
在使用具体的IP地址的使用,直接传入字符串则不需大小端转换,直接使用inet_addr(char*)函数来绑定,如果要使用INADDR_ANY(多网卡IP地址绑定)则需要htonl(INADDR_ANY)来转换,或者要通过一个端口号来绑定套接字的端口号,那么则需要htos(int)来转换。

serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  //本地所有网卡的IP地址
serv_addr.sin_port = htons(8888);  //端口
connect()函数原型

connect函数用于客户端,将一个流类型套接字(TCP协议套接字)与服务端建立连接。
connect函数各个参数的含义与bind()函数完全一样

int connect(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int connect(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows	

connect()调用之后线程会进入阻塞状态,直至成功连接服务端。


listen()和accept()

listen()函数原型

listen函数可以让服务端的一个套接字进入被动监听的状态,这个套接字我们称它为监听套接字。

int listen(int sock, int backlog);  //Linux
int listen(SOCKET sock, int backlog);  //Windows

sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)
可以将backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般较大,可能为几百或者上千。

需要注意的是,listen()函数只是让套接字进入一种监听的状态,并没有真正接受请求,接受请求需要调用accept()函数。

accept()函数原型

当监听套接字处于监听状态时,调用accept()函数可以接受客户端的请求。

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);  //Linux
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);  //Windows

它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。accept()返回一个套接字,这个返回的套接字才是与客户端建立连接的套接字,而不是之前的监听套接字。
和connect类似,accept()也会阻塞线程,直至接受到请求。


send()/recv()和write()/read()

windows下发送、接受
int send(SOCKET sock, const char *buf, int len, int flags);		//发送
int recv(SOCKET sock, char *buf, int len, int flags);			//接受

sock 为要发送数据的套接字,buf 为要发送的数据的缓冲区地址,len 为要发送的数据的字节数,flags 为发送数据时的选项,一般将flags置为0或者NULL。

linux下发送、接受
ssize_t write(int fd, const void *buf, size_t nbytes);		//发送
ssize_t read(int fd, void *buf, size_t nbytes);				//接受

fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。
Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。


close()/closesocket()与shutdown()

调用 close()/closesocket() 函数意味着完全断开连接,即不能发送数据也不能接收数据,这种“生硬”的方式有时候会显得不太“优雅”。

shutdown()函数原型
int shutdown(int sock, int howto);  //Linux
int shutdown(SOCKET s, int howto);  //Windows

sock 为需要断开的套接字,howto 为断开方式。

howto 在 Linux 下有以下取值:
SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。

howto 在 Windows 下有以下取值:
SD_RECEIVE:关闭接收操作,也就是断开输入流。
SD_SEND:关闭发送操作,也就是断开输出流。
SD_BOTH:同时关闭接收和发送操作。

确切地说,close() / closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。
shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。
调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。
默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值