socket编程-基础API
Linux下使用C++进行socket编程。
创建socket
socket
在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件,UNIX/Linux 中的一切都是文件。为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个文件描述符,而网络连接也是一个文件,它也有文件描述符。而Windows 也有类似“文件描述符”的概念,但通常被称为“文件句柄”。通过socket()函数可以创建一个网络连接,得到的返回值就是对应的文件描述符
Internet套接字
通过 socket() 函数创建连接时,必须告诉它使用哪种数据传输方式。最常用的是流格式套接字和数据报格式套接字。
流格式套接字SOCK_STREAM
流格式套接字是“面向连接的套接字”,对应TCP,SOCK_STREAM是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。
SOCK_STREAM有以下特点:
- 数据按序到达
- 发送数据没有数据边界
- 数据传输过程中不会丢失
SOCK_STREAM类型的套接口为全双向的字节流,在接收或发送数据前必需处于已连接状态。用connect()调用建立与另一套接口的连接,连接成功后,即可用send()和recv()传送数据。当会话结束后,调用closesocket()。带外数据根据规定用send()和recv()来接收。
数据报格式套接字SOCK_DGRAM
数据报格式套接字是“无连接的套接字”,对应UDP,SOCK_DGRAM,只管传输数据,不作数据校验,若数据损坏不会重传。
SOCK_DGRAM有以下特点:
- 数据快速传输,可能乱序
- 每次传输数据比较少
- 发送数据有数据边界
SOCK_DGRAM类型套接口允许使用sendto()和recvfrom()从任意端口发送或接收数据报。如果这样一个套接口用connect()与一个指定端口连接,则可用send()和recv()与该端口进行数据报的发送与接收。
创建socket
int socket(int domain,int type,int protocol)
-
domain: 指明系统要使用的底层协议族,对于TCP/IP协议族而言设置为PF_INET,对于UNIXben地域协议族而言设置为PF_UNIX
-
type:socket的类型,如SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)
-
protocol:协议号,默认为0,常用协议为IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。一般情况下有了 domain 和 type 两个参数就可以创建socket了,系统会自动推演出协议类型。
若无错误发生,socket()返回引用新套接口的描述字。否则的话,返回INVALID_SOCKET错误,应用程序可通过WSAGetLastError()获取相应错误代码。
示例代码:
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); //IPPROTO_TCP表示TCP协议
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); //IPPROTO_UDP表示UDP协议
//上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议
int tcp_socket2 = socket(PF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket2 = socket(PF_INET, SOCK_DGRAM, 0); //创建UDP套接字
加载socket库
在Windows条件下需要先加载套接字库
WSAStartup,即WSA(Windows Sockets Asynchronous,Windows异步套接字)的启动命令,WSAStartup必须是应用程序或DLL调用的第一个Windows Sockets函数,允许应用程序或DLL指明Windows Sockets API的版本号及获得特定Windows Sockets实现的细节。
#include "winsock.h"
#include "windows.h"
#pragma comment(lib,"ws2_32.lib") //静态加入一个lib文件
WORD sockVersion = MAKEWORD(2, 2);
/*
WORD是微软SDK中的无符号16位整形数,MAKEWORD(a,b)是一个宏,这里用来指定使用的Winsock版本 ,MAKEWORD(2,2)即版本2.2
*/
WSADATA wsaData;//WSADATA 结构被用来保存函数 WSAStartup 返回的 Windows Sockets初始化信息
if (WSAStartup(sockVersion, &wsaData) != 0) {
/*
使用 Socket 的程序在使用 Socket 之前必须调用 WSAStartup 函数, 当一个应用程序调用 WSAStartup 函数时,
操作系统根据请求的 Socket 版本来搜索相应的 Socket 库,
然后绑定找到的 Socket 库到该应用程序中。以后应用程序就可以调用所请求的 Socket 库中的其它 Socket 函数了。
*/
printf_s("WSAStartup failed.\n"); // 初始化失败
exit(1);
}
/*socket相关操作
....
*/
WSACleanup();//使用完socket后使用WSACleanup()函数来主动释放资源
而在Linux条件下的头文件为
#include<sys/types.h>
#include<sys/socket.h>
命名socket
bind(int sockfd,const struct sockaddr* addr,int addrlen);
- sockfd :socket 文件描述符
- addr: sockaddr 结构体变量的指针
- addrlen: addr 变量的大小,可由 sizeof() 计算得出。
bind()函数通过给一个未命名socket分配一个本地名字(sockaddr)来为套接口建立本地绑定。bind失败时返回-1并设置errno,成功时返回0。
地址相关的操作见socket地址API。
- errno == EACCES:被绑定的地址为受保护的地址,只有root用户能够绑定,例如绑定到了知名端口上
- errno == EADDRINUSE:被绑定的地址正在使用
/*创建一个地址*/
struct sockaddr_in address;
bzero(&address, sizeof( address ));
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
/*创建socket*/
int sock = socket( PF_INET , SOCK_STREAM , 0);
/*绑定socket和address*/
int ret = bind( sock , (struct sockaddr * )&address , sizeof( address));
监听socket
int listen( int sockfd ,int backlog);
- sockfd:被监听的socket文件描述符
- backlog:内核监听队伍的长度
对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数为socket创建一个监听队伍来存放要处理的客户连接,再调用 accept() 函数,就可以随时响应客户端的请求了。监听队伍的长度由backlog决定,当监听队伍满时无法再处理新的客户连接。
接受连接
int accept( int sockfd , struct sockaddr* addr , socklen_t * addrlen);
- sockfd:执行accpet调用的监听socket
- addr:用于保存获取的连接的远端socket地址
- addrlen:socket地址的长度
处于监听状态(listen)的socket通过调用accept()可以从监听队伍中取出一条连接,对应的返回值就是用于与客户端通信的socket,下面的读写操作均通过accept返回的socket进行通信。
struct sockaddr_in client; // 用于接收连接的sockaddr
socklen_t client_addrlength = sizeof( client ); // sockaddr的长度
int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{
char remote[INET_ADDRSTRLEN ];
printf( "connected with ip: %s and port: %d\n",
inet_ntop( AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN ),
//inet_ntop将网络序存储的IP(sin_addr)转为char*并存储在remote中,然后返回
ntohs( client.sin_port ) );
/* ... */
}
accept操作只会从监听队伍中取出连接,而不关心连接处于什么状态(即使客户端已经断开了连接)
发起连接
int connect( int sockfd , const struct sockaddr * serv_addr , socklen_t addrlen );
- sockfd:客户端用于连接服务器的socket
- serv_addr:服务器监听的socket地址,也就是客户端要连接的地址
- addrlen:socket地址的长度
客户端通过connect()函数来主动与服务器建立连接,connect()成功时返回0,此后通过sockfd来和服务器进行通信,失败时返回-1,同时设置errno:
- errno == ECONNREFUSED:目标端口不存在,连接被拒接
- errno == ETIMEOUT:连接超时
关闭连接
int close( int fd );
int shutdown( int sockfd , int howto );
close()系统调用并不是立即关闭一个连接,而是将fd的引用计数减1,当fd的引用计数为0时才真正关闭连接。多进程时fork()会导致fd的引用计数加1,因此必须在父进程和子进程中都调用close()才能关闭连接。
为此引入了shutdown()系统调用,会强制关闭socket,howto参数决定了shutdown()的行为。
数据读写
TCP读写 - send()/recv()
ssize_t recv( int sockfd , void *buf, size_t len, int flags );
ssize_t send( int sockfd , const void *buf, size_t len, int flags );
- sockfd:通信用的socket
- buf:发送/接收缓冲区
- len:缓冲区的大小
- flags:为数据收发提供了额外的控制,一般设置为0/NULL
recv()读取sockfd上的数据,成功时返回读取到的数据的长度,可能需要调用多次recv()才能读到完整的数据;recv()返回0时代表对方已经关闭连接;recv()返回-1说明出错并会设置errno。默认情况下recv()为阻塞I/O,当没有数据时会阻塞等待。
send()向sockfd上写数据,返回值和recv()定义的一致。send()默认也是阻塞I/O,当内核写缓冲满时会阻塞等待。
UDP读写 - sendto()/recvfrom()
ssize_t recvfrom( int sockfd , void *buf, size_t len, int flags ,
struct sockaddr *src_addr , socklen_t addrlen );
ssize_t sendto( int sockfd , const void *buf, size_t len, int flags ,
const struct sockaddr *dest_addr , socklen_t addrlen );
由于UDP中没有建立连接,所以每次读写数据都要获取发送方/接收方的socket地址。
recvfrom()用于读取sockfd上的数据,用法与recv()一样,额外的参数 src_addr 和 addrlen 用于指明发送方的socket地址。
sendto()用于通过sockfd发送数据,用法和send()一样,额外的参数 dest_addr 和 addrlen 用于指明接收方的socket地址。

浙公网安备 33010602011771号