linux基于socket网络编程
socket简介
Socket
接口是TCP/IP
网络的API
,Socket
接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP
网络上的应用程序。要学Internet
上的TCP/IP
网络编程,必须理解Socket
接口。Socket
接口设计者最先是将接口放在Unix
操作系统里面的。如果了解Unix
系统的输入和输出的话,就很容易了解Socket
了。网络的Socket
数据传输是一种特殊的I/O
,Socket
也是一种文件描述符。Socket
也具有一个类似于打开文件的函数调用Socket()
,该函数返 回一个整型的Socket
描述符,随后的连接建立、数据传输等操作都是通过该Socket
实现的。常用的Socket
类型有两种:流式Socket
(SOCK_STREAM
)和数据报式Socket
(SOCK_DGRAM
)。流式是一种面向连接的Socket
,针对于面向连接的TCP
服务应用;数据 报式Socket
是一种无连接的Socket
,对应于无连接的UDP
服务应用。
套接字描述符
其实就是一个整数,我们最熟悉的句柄是
0
、1
、2
三个,0
是标准输入,1
是标准输出,2
是标准错误输出。0
、1
、2
是整数表示的,对应的FILE *
结构的表示就是stdin
、stdout
、stderr
。
套接字API
最初是作为UNIX
操作系统的一部分而开发的,所以套接字API
与系统的其他I/O
设备集成在一起。特别是,当应用程序要为因特网通信而创建一个套接字(socket
)时,操作系统就返回一个小整数作为描述符(descriptor
)来标识这个套接字。然后,应用程序以该描述符作为传递参数,通过调用函数来完成某种操作(例如通过网络传送数据或接收输入的数据)。
在许多操作系统中,套接字描述符和其他I/O
描述符是集成在一起的,所以应用程序可以对文件进行套接字I/O
或I/O
读/写操作。
当应用程序要创建一个套接字时,操作系统就返回一个小整数作为描述符,应用程序则使用这个描述符来引用该套接字需要I/O
请求的应用程序请求操作系统打开一个文件。操作系统就创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。
对于每个程序系统都有一张单独的表。精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。
socket()描述字建立函数
建立Socket
,程序可以调用Socket
函数,该函数返回一个类似于文件描述符的句柄。socket
函数原型为:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数
- domain:
指明所使用的协议族,通常为AF_INET
,表示互联网协议族(TCP/IP
协议族);
AF_INET
IPv4
因特网域AF_INET6
IPv6
因特网域AF_UNIX
Unix
域AF_ROUTE
路由套接字AF_KEY
密钥套接字AF_UNSPEC
未指定- type参数指定
socket
的类型:
- SOCK_STREAM:
流式套接字提供可靠的、面向连接的通信流;它使用TCP
协议,从而保证了数据传输的正确性和顺序性- SOCK_DGRAM
数据报套接字定义了一种无连接的服 ,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP
。- SOCK_RAW
允许程序使用低层协议,原始套接字允许对底层协议如IP
或ICMP
进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。- protocol
通常赋值”0
“。
0
选择type
类型对应的默认协议IPPROTO_TCP
TCP
传输协议IPPROTO_UDP
UDP
传输协议IPPROTO_SCTP
SCTP
传输协议IPPROTO_TIPC
TIPC
传输协议返回值
- 返回一个整型
socket
描述符,可以在后面的调用中使用它。socket
描述符是一个整数,但它指向内部数据结构(socket
结构)的指针,它指向描述符表入口。调用socket
函数时,socket
执行体将建立一个socket
结构,实际上”建立一个socket
“意味着为一个socket
数据结构分配存储空间。socket
执行体为你管理描述符表。该空间还有许多成员,并不会在初始化时被填充,可调用其它函数进行填充完善。
两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。socket
数据结构中包含这五种信息.
bind()函数:IP号端口号与相应描述字赋值函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能
- 用于绑定
IP
地址和端口号到socketfd
参数
- sockfd
是一个socket
描述符- addr
是一个指向包含有本机IP
地址及端口号等信息的sockaddr
类型的指针,指向要绑定给sockfd
的协议地址结构,这个地址结构根据地址创建socket
时的地址协议族的不同而不同
//ipv4对应的是:
struct sockaddr{
unisgned short as_family; // 协议族
char sa_data[14]; // IP+端口
};
同等替换:
struct sockaddr_in {
sa_family_t sin_family; /* 协议族 */
in_port_t sin_port; /* 端口号*/
struct in_addr sin_addr; /*IP地址结构体*/
unsigned char sin_zero[8]; /* 填充 没有实际意义,只是为跟sockaddr结构在内存中对齐,这样两者才能相互转换*/
};
/*两个结构是等同的可以先互转换,第一个结构将地址和端口绑定了,第二个结构将两者分开表示*/
/* IP地址结构如下:为32位字 */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
//ipv6对应的是:
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
//Unix域对应的是:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
addrlen
- 对应的是地址的长度,常被设置为
sizeof(struct sockaddr)
返回值
bind()
函数在成功被调用时返回0
;遇到错误时返回-1
并将errno
置为相应的错误号对于
TCP
协议,调用bind
函数可以指定一个端口号,或指定一个IP
地址,也可以两者都指定,还可以都不指定。若TCP
客户端或服务器端不调用bind
函数绑定一个端口号,当调用connect
或listen
函数时,内核会为相应的套接字选择一个临时端口号。一般TCP
客户端使用内核为其选择一个临时的端口号,而服务器端通过调用bind
函数将端口号与相应的套接字绑定。进程可以把一个特定的IP
地址捆绑到它的套接字上,但是这个IP
地址必须属于其所在主机的网络接口之一。对于TCP
客户端,这就为在套接字上发送的IP
数据报指派了源IP
地址。对于TCP
服务器端,这就限定该套接字只接收那些目的地为这个IP
地址的客户端连接。TCP
客户端一般不把IP
地址捆绑到它的套接字上。当连接套接字时,内核将根据所用外出网络接口来选择源IP
地址,而所用外出接口则取决于到达服务器端所需的路径。若TCP
服务器端没有把IP
地址捆绑到它的套接字上,内核就把客户端发送的SYN
的目的IP
地址作为服务器端的源IP
地址。
connect()函数:客户机连接主机
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能
- 该函数用于绑定之后的
client
端(客户端),与服务器建立连接参数
- sockfd
是目的服务器的sockect
描述符- addr
是服务器端的IP
地址和端口号的地址结构指针- addrlen
地址长度常被设置为sizeof(struct sockaddr)
返回值
- 成功返回
0
,遇到错误时返回-1
,并且errno
中包含相应的错误码
listen()函数:监听设置函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能
- 设置能处理的最大连接数,
listen()
并未开始接受连线,只是设置sockect
的listen
模式 ,listen
函数只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。主要就两个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。- 内核为任何一个给定监听套接字维护两个队列:
- 未完成连接队列,每个这样的
SYN
报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TCP
三次握手过程。这些套接字处于SYN_REVD
状态;- 已完成连接队列,每个已完成
TCP
三次握手过程的客户端对应其中一项。这些套接字处于ESTABLISHED
状态;参数
- sockfd
sockfd
是socket
系统调用返回的服务器端socket
描述符- backlog
backlog
指定在请求队列中允许的最大请求数
accept()函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能
accept
函数由TCP
服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。参数
- sockfd
sockfd
是socket
系统调用返回的服务器端socket
描述符- addr
用来返回已连接的对端(客户端)的协议地址- addrled
客户端地址长度返回值
- 该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示
TCP
三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
shutdown()
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
功能
- 关闭套接字
参数
- sockfd
sockfd
是socket
系统调用返回的服务器端socket
描述符- howto
- 1.
SHUT_RD
:值为0
,关闭连接的读这一半;- 2.
SHUT_WR
:值为1
,关闭连接的写这一半;- 3.
SHUT_RDWR
:值为2
,连接的读和写都关闭。
close()
#include <unistd.h>
int close(int sockfd); //彻底关闭套接字
getsockname ,getpeername 函数
/*
* 函数功能:获取已绑定到一个套接字的地址;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp);
/*
* 说明:
* 调用该函数之前,设置alenp为一个指向整数的指针,该整数指定缓冲区sockaddr的大小;
* 返回时,该整数会被设置成返回地址的大小,如果该地址和提供的缓冲区长度不匹配,则将其截断而不报错;
*/
/*
* 函数功能:获取套接字对方连接的地址;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp);
/*
* 说明:
* 该函数除了返回对方的地址之外,其他功能和getsockname一样;
*/
当不调用
bind
或调用bind
没有指定本地IP地址和端口时,可以调用函数getsockname
来返回内核分配的本地IP
地址和端口号,还可以获取到套接口的协议簇。当一个新的连接建立时,服务器端也可以调用getsockname
来获取分配给此连接的本地IP
地址;当服务器端的一个子进程调用函数exec
启动执行时,只能调用getpeername
来获取客户端IP
地址和端口号。
套接字通信流程
大多数
TCP
服务器是并发的,它们为每一个待处理的客户连接调用fork
派生一个子进程,当一个连接建立时,accept
返回,服务器调用fork
函数新建一个子进程,由子进程处理与客户的连接,父进程则可以在监听套接字上再次调用accept
来处理下一个客户的连接。
所有的客户端和服务器都是从调用socket
开始的,它返回一个描述符,客户端调用connect
,服务器调用bind
,listen
,accept
。建立连接之后I/O
函数进行数据的传递。通信结束一般用close
关闭套接字。
补充知识
套接口即网络进程的
ID
;网络通信归根到底即为进程间的通信;套接字中包含了端口号,用来确定进程,一个端口号一次只能分配给一个进程,即端口号与进程是一一对应的;
辅助函数
1、字节排序函数
#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue); //返回网络字节序的值
uint32_t htonl(uint32_t host32bitvalue); //返回网络字节序的值
uint16_t ntohs(uint16_t net16bitvalue); //返回主机字节序的值
uint32_t ntohl(uint32_t net32bitvalue); //返回主机字节序的值
这几个函数主要的作用就是讲主机字节序的地址转化为网络字节序,什么意思呢?计算机处理器型号的不同,数据存储方式有所不同,偷得采用大端存储,有的采用小端存储,例如:同样存储
0x1234
,大小端机器存储顺序是相反的,但网络IP地址是统一规定的,这些函数就是将机器大小端存储造成的差异取消掉,使之将他们变为标准IP
,这些函数中,h
代表host
,n
代表net
,s
代表short
(两个字节),l
代表long
(4
个字节),通过上面的4
个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR_ANY
,INADDR_ANY
指定地址为0.0.0.0
的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。
2、字节操作函数
字节操作函数主要是用于操作结构体中的某几个字节,在处理套接字结构中有
IP
地址等字段,包含0
字节却不是C
字符串,需要特定的函数来进行处理。不能直接赋0
#include <string.h>
void bzero(void *dest,size_t nbytes);
void bcopy(const void *src,void *dest,size_t nbytes);
int bcmp(const void *prt1,const void *ptr2,size_t nbytes); //相等返回0,否则非0
void *memset(void *dest,int c,size_t len);
void *memcpy(void *dest,const void *src,size_t nbytes);
int memcmp(const void *ptr1,const void *ptr2,size_t nbytes); //相等返回0,否则非0
以
b
打头的函数为支持套接口函数的系统所提供,mem
为支持ANSI C
库提供的函数;其中,bzero
将指定数目的字节设置为0
,bcopy
将指定数目的字节从源字节串复制到目的字节串。bcmp
比较两个字节串,相同返回0
。memset
将目标中指定数据的字节设置为指定的值(不一定是0
),memcpy
为复制,memcmp
为比较,两者与前面类似。
3、地址转换函数
一般使用
ASCII
字符串表示IP
地址,也就是点分十进制数表示(218.170.19.1
),但在套接字中使用32
位网络字节序,保存的是二进制值,因此必须进行地址转换。
#include <arpa/inet.h>
in_addr_t inet_addr(const char *straddr); //字符串有效返回网络字节序的IP地址,否则INADDR_NONE
int inet_aton(const char* straddr,struct in_addr *addrp);//字符串有效返回1,否则0
char* inet_ntoa(struct in_addr inaddr); //返回一个字符串指针
int inet_pton(int family, const char* str, void* addr);
//成功返回1,字符串无效返回0,出错返回-1
const char* inet_ntop(int family, const void* addr, char* str, size_t len);
//成功返回结果指针,出错返回NULL
/*说明
*后面两个函数为新函数,支持IPv4和IPv6,family用来指定:AF_INET,AF_INET6
*inet_ntop函数len为目标存储单元大小,str指针就是函数返回值
*/
inet_aton
将一个字符串转换为32
位网络字节序二进制值,用结构in_addr
存储。inet_addr
功能相同(废弃,不使用),inet_ntoa
进行相反的操作。
4、字节流读取函数
在套接字通信中进行字节读取函数:
read()
,write()
。与I/O
中的读取函数略有区别,因为它们输入或输出的字节数比可能比请求的少。
ssize_t write(int fd, const void*buf,size_t nbytes);
ssize_t read(int fd,void *buf,size_t nbyte);
/*说明
*函数均返回读或写的字节个数,出错则返回-1
*/
第一个将
buf
中的nbytes
个字节写入到文件描述符fd
中,成功时返回写的字节数。第二个为从fd
中读取nbyte
个字节到buf
中,返回实际所读的字节数。详细应用说明参考使用read
write
读写socket
(套节字) 。
网络I/O
还有一些函数,例如:recv()
/send()
,readv()
/writev()
,recvmsg()
/sendmsg()
,recvfrom()
/sendto
()等
5、在TCP套接字上发送数据函数:有连接
ssize_t send(int s,const void *msg,size_t len,int flags);
//包含3要素:套接字s,待发数据msg,数据长度len
//函数只能对处于连接状态的套接字使用,参数s为已建立好连接的套接字描述
//符,即accept函数的返回值
//参数msg指向存放待发送数据的缓冲区
//参数len为待发送数据的长度,参数flags为控制选项,一般设置为0
6、在TCP套接字上接收数据函数:有连接
ssize_t recv(int s,void *buf,size_t len,int flags);
//包含3要素:套接字s,接收缓冲区buf,长度len
//函数recv从参数s所指定的套接字描述符(必须是面向连接的套接字)上接收
//数据并保存到参数buf所指定的缓冲区
//参数len则为缓冲区长度,参数flags为控制选项,一般设置为0
7、在UCP套接字上发送数据函数:无连接
ssize_t sendto(int s,const void *msg,size_t len,int flags,const struct sockaddr *to,socklen_t tolen);
//函数功能与函数send类似,但函数sendto不需要套接字处于连接状态,所以
//该函数通常用来发送UDP数据,同时因为是无连接的套接字,在使用sendto时
//需要指定数据的目的地址,参数msg指向待发送数据的缓冲区。
//参数len指定了待发送数据的长度
//参数flags是控制选项,含义与send函数中的一致
//参数to用于指定目的地址,目的地址的长度由tolen指定
8、在UDP套接字上接收数据函数:无连接
ssize_t recvfrom(int s ,void *buf,size_t len,int flags,struct sockaddr *from,socklen_t *fromlen);
//与函数recv功能类似,只是函数recv只能用于面向连接的套接字,而函数
//recvfrom没有此限制,可以用于从无连接的套接字上接收数据
//参数buf指向接收缓冲区
//参数len指定了缓冲区的大小
//参数flags是控制选项,含义与recv中的一致
//如果参数from非空,且该套接字不是面向连接的,则函数recvfrom返回时,
//参数from中将保存数据的源地址
//参数fromlen在调用recvfrom前为参数from的长度,调用recvfrom后将
//保存from的实际大小
网络编程示例,写一个简单的QQ程序
服务器端
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdio.h>
#include<time.h>
#define port 8888
char* getDateTime();
int main()
{
//定义服务器 客户端的套接字描述符
int sfd=0,cfd=0,ret=0,addrlen=0,count=0;
char * nowtime;
//定义接收缓存区
unsigned char ReadBuffer[1024];
unsigned char SendBuffer[1024];
//定义服务器 客户端的套接字数据结构
struct sockaddr_in saddr,caddr;
//数据结构清0
memset(&saddr,0,sizeof(struct sockaddr));
memset(&caddr,0,sizeof(struct sockaddr));
//创建服务器套接字
sfd=socket(AF_INET,SOCK_STREAM,0);
//判断
if(sfd == -1)
{
perror("socket");
exit(-1);
}
//初始化服务器套接字数据结构
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = INADDR_ANY;
bzero(saddr.sin_zero,8);
//套接字绑定
ret = bind(sfd,(struct sockaddr*)(&saddr),sizeof(struct sockaddr));
if(ret == -1)
{
perror("bind");
exit(-1);
}
//监听设置
ret=listen(sfd,10);
if(ret == -1)
{
perror("listen");
exit(-1);
}
while(1)
{
//接收等待
addrlen = sizeof(struct sockaddr);
cfd=accept(sfd,(struct sockaddr*)(&caddr),&addrlen);
if(cfd == -1)
{
perror("accept");
}
printf("从机ip是%s\n",inet_ntoa(caddr.sin_addr));
if(fork()==0)
{
while(1)
{
if(fork()==0)
{
fgets(SendBuffer,1024,stdin);
send(cfd,SendBuffer,1024,0);
printf("\n");
}
nowtime=getDateTime();
count= recv(cfd,ReadBuffer,1024,0);
//printf("count is %d\n",count);
ReadBuffer[count]='\0';
printf("李京京 %s\n",nowtime);
printf("%s\n",ReadBuffer);
}
}
}
}
char* getDateTime()
{
static char nowtime[20];
time_t rawtime;
struct tm* ltime;
time(&rawtime);
ltime=localtime(&rawtime);
strftime(nowtime,20,"%Y-%m-%d %H:%M:%S",ltime);
return nowtime;
}
客户端
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdio.h>
#include<time.h>
#define port 8888
char* getDateTime();
int main(int argc,char **argv)
{
//定义服务器 客户端的套接字描述符
int cfd=0,ret=0;
char * nowtime;
//定义接收缓存区
unsigned char SendBuffer[1024];
unsigned char ReadBuffer[1024];
//定义服务器 客户端的套接字数据结构
struct sockaddr_in saddr,caddr;
//数据结构清0
memset(&saddr,0,sizeof(struct sockaddr));
memset(&caddr,0,sizeof(struct sockaddr));
//创建服务器套接字
cfd=socket(AF_INET,SOCK_STREAM,0);
//判断
if(cfd == -1)
{
perror("socket");
exit(-1);
}
//初始化服务器套接字数据结构
caddr.sin_family = AF_INET;
caddr.sin_port = htons(port);
inet_aton(argv[1],&caddr.sin_addr);
bzero(caddr.sin_zero,8);
ret = connect(cfd,(struct sockaddr*)(&caddr),sizeof(struct sockaddr));
if(ret == -1)
{
perror("ret");
exit(-1);
}
while(1)
{
if(fork()==0)
{
nowtime=getDateTime();
ret=recv(cfd,ReadBuffer,1024,0);
printf("张超 %s\n",nowtime);
ReadBuffer[ret]='\0';
printf("%s\n",ReadBuffer);
}
fgets(SendBuffer,1024,stdin);
send(cfd,SendBuffer,1024,0);
printf("\n");
}
}
char* getDateTime()
{
static char nowtime[20];
time_t rawtime;
struct tm* ltime;
time(&rawtime);
ltime=localtime(&rawtime);
strftime(nowtime,20,"%Y-%m-%d %H:%M:%S",ltime);
return nowtime;
}
更多推荐
所有评论(0)