Linux C语言socket网络编程
注意:本文是按照 TCP、UDP的工作过程进行总结的
- TCP套 socket 接口编程:基于TCP的 客户/服务器(C/S)模式的工作过程如下:
服务器进程中的一些函数:
- socket():
/* 函数所需头文件及其原型 */ #include <sys/socket.h> int socket( int family, int type, int protocol); socketfd = soket(AF_INET, SOCK_STREAM, 0); /* socketfd 作为返回值,可以记作描述符。 若 socketfd 非负则表示成功,为负则表示失败。 参数: family -> 指明协议族 type -> 字节流类型 protocol -> 一般置0. 参数 family 的取值范围是: AF_LOCAL UNIX 协议族 AF_ROUTE 路由套接口 AF_INET IPv4 协议 AF_INET6 IPv6 协议 AF_KEY 密钥套接口 参数 type 的取值范围: SOCK_STREAM TCP 套接口 SOCK_DGRAM UDP 套接口 SOCK_PACKET 支持数据链路访问 SOCK_RAM 原始套接口 */
生成套接口描述字(套接字)后,要为套接口的地址数据结构进行赋初值。
通用套接口地址的数据结构中,struct sockaddr_in 需要掌握:
struct in_addr { in_addr_t s_addr; /*32 位 IP 地址,网络字节序*/ }; struct sockaddr_in { uint8 sin_len; sa_family_t sin_family; in_port_t sin_port; /*16 位端口号,网络字节序*/ struct in_addr sin_addr; char sin_zero[8]; /*备用的域,未使用*/ };
PS:需要注意的是,一般在 socket() 之后,我们会填写 sockaddr 的相关内容。
/* Fill the local socket address struct */ memset (&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; // Protocol Family servaddr.sin_port = htons (PORT); // Port number servaddr.sin_addr.s_addr = htonl (INADDR_ANY); // AutoFill local address
- bind():
// 函数原型:
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
/*
参数 sockfd :套接字描述符。
参数 my_addr:指向 sockaddr 结构体的指针(该结构体中保存有端口和 IP 地址 信息)。
参数 addlen:结构体 sockaddr 的长度。
返回:0──成功, -1──失败
*/
ret = bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr));
/* 功能:当调用 socket 函数创建套接字后,该套接字并没有与 本机地址和端口等 信息相连,
bind 函数将完成这些工作。
*/
- listen():
// 函数原型:
#include <sys/socket.h>
#include<sys/types.h>
// #define BACKLOG 10
int listen(int sockfd,int backlog);
/*
参数 sockfd :套接字描述符。
参数 backlog :规定内核为此套接口排队的最大选择个数。
*/
ret = listen(sockfd,BACKLOG);
// 通常采用一下的异常处理:
if(listen(listenfd,BACKLOG) == -1){
printf("ERROR: Failed to listen Port %d.\n", PORT);
return (0);
}
else{
printf("OK: Listening the Port %d sucessfully.\n", PORT);
}
处在监听模式下后,程序就需要一个循环来实现挂起等待客户机请求。所以接下来的一步就是 接受客户机的请求。
- accept():
先来了解一下 accept() 这个函数:
// 函数原型:
#include <sys/socket.h>
#include<sys/types.h>
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen);
/*
sockfd 参数:监听的 套接字描述符。
cliaddr 参数:指向结构体 sockaddr 的指针。
addrlen 参数:cliaddr 参数指向的内存空间的长度。
*/
sin_size = sizeof(struct sockaddr_in);
connect_fd = accept(sockfd,( struct sockaddr *)&their_addr,&sin_size);
accept() 函数用于面向连接类型的套接字类型。
accept() 函数将从连接请求队列中获得连接信息,创建新的套 接字,并返回该套接字的文件描述符。
新创建的套接字用于服务器与客户机的通信,而原来的套接字仍然处于监听状态。
它们的区别在于:监听套接口描述字 只有一个,而且一直存在,
每一个连接都有一个已连接套接口描述字,当连接断开 时就关闭该描述字。
注意:bind 函数和 accept 函数的第三个参数是不一样的。
- close():
// 函数原型:
#include <unistd.h>
int close(int sockfd);
// 成功则返回 0,否则返回-1。
// 功能:关闭套接口 其中参数 sockfd 是关闭的套接口描述字。
// 当对一个套接口调用 close()时, 关闭该套接口描述字,并停止连接。
以后这个套接口不能再使用,也不能再执行 任何读写操作,但关闭时已经排队准备发送的数据仍会被发出 使用完一个套接口后,一定要记得将它关掉,任何一个文件读写操作完毕之后, 都要关闭它的描述字。
客户机进程中的一些函数:
- socket():这个函数前面提过,这里不必多说。
创建套接字后,同理,也需要对套接口进行设置: (这是在客户端填充的服务器 端的资料)......
bzero(&server_addr,sizeof(server_addr)); // 初始化,置 0 server_addr.sin_family=AF_INET; // IPV4 server_addr.sin_port=htons(portnumber); // (将本机器上的 short 数据转化为网络上的 short 数据)端口号,与服务器端 的端口号相同 server_addr.sin_addr=*((struct in_addr *)host->h_addr_list); // IP 地址
- connect():
connect(sockfd,(struct sockaddr *)(&server_addr), sizeof(structsockaddr));
函数原型:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *serv_addr,int addrlen);
/*
返回值:成功:返回 0 错误:返回-1,并将全局变量 errno 设置为相应的错误号。
参数 sockfd :数据发送的套接字,解决从哪里发送的问题,ockfd 是先前 socket 返回的值
参数 serv_addr:据发送的目的地,也就是服务器端的地址
参数 addrlen:指定 server_addr 结构体的长度
*/
函数功能:
创建了一个套接口之后,使客户端和服务器连接。其实就是完成一个 有连接协议 的连接过程,
对于 TCP 来说就是那个三段握手过程。
关于三段握手:( 《计算机网络》谢希仁编著 第七版中 将其定名为:" 三报文握手"):
客户端先用 connect() 向服务器发出一个要求连接的信号 SYN1;
服务器 进程接收到这个信号后,发回应答信号 ack1,同时这也是一个要求回答的信号 SYN2;
客户端收到信号 ack1 和 SYN2 后,再次应答 ack2; 服务器收到应答信号 ack2,一次连接才算建立完成。
从上面过程可以看出,服务器会收到两次信 号 SYN1 和 ack2,因此服务器进程需要两个队列保存不同状态的连接。刚接收 到 SYN1 信号时,连接还未完成,这时的连接放在一个名为“未完成连接”的队列中。接收到 ack2 信号后,三段握手完成,这时的连接放在名为“已完成连接” 的队列中,等待 accept() 调用。
关于 recv() 、send() 和 recvfrom() 、sendto() :
- 先说前两个:recv() 和 send() 都是基于 TCP 协议。
不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。
客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。
同样,不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。
// 函数原型: int send( SOCKET s, const char *buf, int len, int flags ); int recv( SOCKET s, char *buf, int len, int flags );
(1)recv 先等待 s 的发送缓冲中的数据被协议传送完毕,如果协议在传送 s 的发送缓冲中的数据时出现网络错误,那么recv函数返回 SOCKET_ERROR ;
(2)如果 s 的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv 先检查套接字 s 的接收缓冲区,如果 s 接收缓冲区中没有数据或者协议正在接收数据,那么 recv 就一直等待,直到协议把数据接收完毕。
当协议把数据接收完毕,recv 函数就把 s 的接收缓冲中的数据 copy 到 buf 中(注意协议接收到的数据可能大于 buf 的长度,所以在这种情况下要调用几次 recv 函数才能把s的接收缓冲中的数据 copy 完。recv 函数仅仅是 copy 数据,真正的接收数据是协议来完成的);
其中,recv 函数返回其实际 copy 的字节数。如果 recv 在 copy 时出错,那么它返回 SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回 0。
- 然后是后两个:recvfrom() 和 sendto() 都是基于 UDP 协议。
不同于 TCP 协议,UDP 提供的是一种无连接的、不可靠的数据包协议。它不对数据进行确认、出错重传、排序等可靠性处理,但是它却具有代码小、速度快和系统开销小等优点。对于某些应用程序,使用 UDP 来实现,将带来更大效率。
与基于 TCP 协议的客户机/服务器模式的工作流程图相比较,它们的主要区别 在于:
使用 TCP 套接口必须先建立连接(例如客户进程的 connect() ,服务器进程 的 **listen() **和 accept() ) 。
而 UDP 套接口不需预先连接,它在调用 socket()生成一个套接口后,
-> 在服务器端调用 bind() 绑定众所周知的端口后, 服务器阻塞于 recvfrom() 调用,
-> 客户端调用 sendto() 发送数据请求,阻塞于 recvfrom() 调用,
-> 服务器端调用 recvfrom() 接收数据,服务器端也调用 sendto() 向客户发送数据作为应答,然后阻塞于 recvfrom() 调用,
-> 客户端 调用 recvfrom() 接收数据......
当数据传输完成以后,UDP 套接口中的客户端调用 close() 断开连接,而 TCP 套接口中的客户端不必再发出“断开连接信号”来通知服务器端关闭连接。
一些重要的应用程序,如域名服务系统 DNS、网络文件 系统 NFS 都使用 UDP 套接口。
// 函数原型: #include <sys/socket.h> int recvfrom(int sockfd, void *buff, int len,int flags, struct sockaddr *fromaddr, int *addrlen); /* 参数 sockfd 为套接口描述字; 参数 buff 为指向读缓冲的指针; 参数 len 为读的字节数; 参数 flags 一般设置为 0; 参数 fromaddr 为指向数据接收的套接口地址结构的指针; 参数 addrlen 为套接口结构长度。 函数返回实际读的字节数,可以为 0,如果出错,则返回-1。 */ int sendto(int sockfd, void *mes,int len, int flags, struct sockaddr *toaddr, int *addrlen); /* 参数 mes 为指向写缓冲的指针; 参数 toaddr 为指向数据发送的套接口地址结构的指针; 函数返回实际写的字节数,可以为 0,如果出错,则返回-1。 */
来源:https://www.jianshu.com/p/f19578165383
socket - 创建一个通信端点
概要:
#include /* See NOTES */
#include
int socket(int domain, int type, int protocol);
描述:
socket()创建一个用于通信的端点,并返回引用该端点的文件描述符。
domain参数指定通信域; 这个参数选定用于通信的协议族。
这些协议域在中定义。目前理解的格式包括:
名称 目的 操作说明
AF_UNIX, AF_LOCAL 本地通信 unix(7)
AF_INET IPv4因特网协议 ip(7)
AF_INET6 IPv6因特网协议 ipv6(7)
AF_IPX IPX - Novell协议
AF_NETLINK 内核用户接口驱动 netlink(7)
AF_X25 ITU-T X.25/ISO-8208协议 x25(7)
AF_AX25 Amateur radio AX.25 协议
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK Mac机所用的网络协议之一 ddp(7)
AF_PACKET 低层数据包接口 packet(7)
AF_ALG 内核加密API接口
socket具有指定的type,用以指定通信语义。目前定义的类型有:
SOCK_STREAM 提供顺序的,可靠的双向的基于连接的字节流。
可能支持带外数据传输机制。
SOCK_DGRAM 支持数据报(无连接,最大长度固定的不可靠消息)。
SOCK_SEQPACKET 为固定最大长度的数据报提供顺序可靠的双向的基于连接的
数据传输路径; 每个输入系统调用需要消费者读取整个数据包。
SOCK_RAW 提供原始网络协议访问。
SOCK_RDM 提供不保证顺序的可靠数据报层。
SOCK_PACKET 过时的且不应该在新程序中使用; 见packet(7)。
所有协议族可能都不实现某些套接字类型。
自从2.6.27版本以来,type参数用于第二个目的:除了指定套接字类型之外,
它还可能包含以下任何值的按位OR,以修改socket()的行为:
SOCK_NONBLOCK 在新打开的文件描述中设置O_NONBLOCK文件状态标志。
使用此标志可以节省对fcntl(2)的额外调用,以获得相同的结果。
SOCK_CLOEXEC 在新的文件描述符上设置close-on-exec (FD_CLOEXEC)标志。
有关这可能有用的原因,请参阅open(2)中的O_CLOEXEC标志的说明。
参数protocol指定了与socket一起使用的特定协议。通常对给定protocol
family中的特定socket type,只存在一个protocol可以支持,在这种情况下
protocol可以指定为0。然而,可能存在很多protocol,在这种情况下,
必须以这种方式指定特定的协议。protocol number用于指定进行通信的
"communication domain";见protocols(5)。关于如何将协议名称字符串映射到协议号,
请参阅getprotoent(3)。
SOCK_STREAM类型的套接字是全双工字节流。它们不保留记录边界。流套接字必须处于
连接状态,才能发送或接收任何数据。使用connect(2)调用创建与另一个套接字的连接。
一旦连接,可以使用read(2)和write(2)调用或send(2)和recv(2)调用的某些变体来
传输数据。会话完成后,可以执行close(2)。带外数据也可以如send(2)中所述发送,
如recv(2)所述接收。
通信协议SOCK_STREAM的实现确保了数据不丢失或重复。如果对等协议具有缓冲区空间
的一条数据不能在合理的时间长度内成功发送,那么连接被认为是死机。当在套接字上
启用SO_KEEPALIVE时,如果另一端仍然存在,协议将以协议指定的方式进行检查。如果
程序在broken stream中发送或接收,则会产生SIGPIP信号; 这导致naive processes,
该程序不处理信号,直接退出。SOCK_SEQPACKET套接字采用与SOCK_STREAM套接字相同
的系统调用。唯一的区别是read(2)调用将仅返回所请求的数据量,并且到达的数据包
中剩余的任何数据将被丢弃。传入数据报中的所有消息边界也被保留。
SOCK_DGRAM和SOCK_RAW sockets允许将数据报发送到在sendto(2)调用中命名的通讯员。
数据报通常用recvfrom(2)接收,它返回下一个数据报及其发送者的地址。
SOCK_PACKET是一种过时的套接字类型,可直接从设备驱动程序接收原始数据包。使用
packet(7)代替。
fcntl(2)F_SETOWN操作可用于指定进程或进程组,以接收外带数据到达时的信号SIGURG,
或接收连接意外断开时的信号SOCK_STREAM。此操作也可以通过使用SIGIO设置进程或进程组
接收I/O和I/O事件的异步通知。使用F_SETOWN等效于使用FIOSETOWN或SIOCSPGRP参数的
ioctl(2)调用。
当网络向协议模块发送错误状况(例如,使用IPMP消息的IP)时,为套接字设置挂起的错误
标志。此套接字上的下一个操作将返回挂起错误的错误代码。对于某些协议,可以启用每个
套接字错误队列来检索关于错误的详细信息; 请参阅ip(7)中的IP_RECVERR。
套接字的操作由套接字级别选项控制。这些选项在中定义。
函数setsockopt(2)和getsockopt(2)分别用于设置和获取选项。
返回值:
成功时,将返回新套接字的文件描述符,
出错时,将返回-1,相应的错误信息记录在errno中。
错误:
EACCES 创建指定类型和/或协议的套接字的权限被拒绝。
EAFNOSUPPORT 该实现不支持指定的地址族。
EINVAL 未知协议或协议族不可用
EINVAL type参数无效
EMFILE 已达到每个进程的打开文件描述符数的限制。
ENFILE 已经达到了系统的打开文件总数的限制。
ENOBUFS 或 ENOMEM 内存不足。在释放足够的资源之前,无法创建套接字。
EPROTONOSUPPORT 该域(domain)中不支持协议类型或指定的协议。
底层协议模块可能会生成其他错误。
遵循:
POSIX.1-2001, POSIX.1-2008, 4.4BSD.
SOCK_NONBLOCK和SOCK_CLOEXEC标志是Linux特有的。
socket()出现在4.2BSD中。通常对支持BSD套接层(包括系统V变体)克隆的
非BSD系统是可移植的。
注意:
POSIX.1不要求包含,且Linux也不要求包含这个头文件。然而,
然而,一些历史的(BSD)实现需要这个头文件,并且对可移植程序来说,
包含它是可能是明智的选择。
在4.x BSD下用于协议族的清单常数是PF_UNIX,PF_INET等,AF_UNIX,AF_INET等
用于地址族。然而,BSD手册页已经承诺:“协议族通常与地址族相同”,随后的
标准使用AF_*。
在Linux 2.6.38中添加了AF_ALG协议类型。有关此接口的更多信息,请参阅内核HTML文档:
https://www.kernel.org/doc/htmldocs/crypto-API/User.html。
示例:
getaddrinfo(3)中展示了使用socket()的示例。
另请参考:
accept(2), bind(2), close(2), connect(2), fcntl(2), getpeername(2),
getsockname(2), getsockopt(2), ioctl(2), listen(2), read(2), recv(2),
select(2), send(2), shutdown(2), socketpair(2), write(2),
getprotoent(3), ip(7), socket(7), tcp(7), udp(7), unix(7)
“一个4.3BSD进程间通信教程介绍”和“BSD进程间通信教程”,
转载于UNIX Programmer's Supplementary Documents Volume 1。
版本记录:
本页面是Linux man-pages项目的4.10版本的一部分。
有关项目的描述,相关错误报告信息以及本页面的最新版本,
请参见https://www.kernel.org/doc/man-pages/。
Linux 2015-12-28 SOCKET(2)