视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
socket编程网络通信
2025-10-04 04:05:13 责编:小OO
文档
网络编程之SOCKET

Socket在所有网络操作系统和网络应用程序中都是必不可少的,它是网络通信中应用进程和网络协议之间的接口。在Linux操作系统中,socket属于文件系统的一部分,网络通信可以看作是对文件的读取。这就使得用户对网络的控制像对文件的控制一样方便。

要了解socket就必须要了解一些基本的概念,如:套接口、网络编程的结构等。下面分别讲述这些概念。

一、基本概念:

1.套接口:

简单地说,套接口就是一种使用UNIX系统中的文件描述符和系统进程通信的一种方法。因为在UNIX系统中,所有的I/O操作都是通过读写文件描述符而产生的。文件描述符就是一个和打开的文件相关连的整数。但文件可以是一个网络连接、一个FIFO、一个管道、一个终端、一个真正存储在磁盘上的文件或者UNIX系统中的任何其他的东西。所以,如果你希望通过Internet和其他的程序进行通信,你只有通过文件描述符。使用系统调用socket(),你可以得到socket()描述符。然后你可以使用send()和recv()调用而与其他的程序通信。你也可以使用一般的文件操作来调用read()和write()而与其他的程序进行通信,但send()和recv()调用可以提供一种更好的数据通信的控制手段。

2.Internet套接口

有两种最常用的Internet套接口,“数据流套接口”和“数据报套接口”,我们用“SOCK_STREAM”和“SOCK_DGRAM”分别代表上面两种套接口。数据报套接口有时也叫做“无连接的套接口”。数据流套接口是可靠的双向连接的通信数据流。如果你在套接口中以“ 1, 2”的顺序放入两个数据,它们在另一端也会以“1, 2”的顺序到达。它们也可以被认为是无错误的传输。经常使用的telnet应用程序就是使用数据流套接口的一个例子。使用HTTP的WWW浏览器也使用数据流套接口来读取网页。事实上,如果你使用telnet登录到一个WWW站点的80端口,然后键入“GET网页名”,你将可以得到这个HTML页。数据流套接口使用TCP得到这种高质量的数据传输。数据报套接口使用UDP,所以数据报的顺序是没有保障的。数据报是按一种应答的方式进行数据传输的。

3.数据结构

下面我们要讨论使用套接口编写程序可能用到的数据结构。

struct sockaddr

{

unsigned short int sa_family;

char sa_data[14];

};

sa_family 为调用socket()时的domain参数,即AF_xxxx值。

sa_data最多使用14个字符长度。

此sockaddr结构会因使用不同的socket domain而有不同结构定义,例如使用AF_INET domain,其socketaddr结构定义便为:

struct socketaddr_in

{

unsigned short int sin_family;

uint16_t sin_port;

struct in_addr sin_addr;

unsigned char sin_zero[8];

};

struct i

n_addr

{

uint32_t s_addr;

};

sin_family 即为sa_family

sin_port 为使用的port编号

sin_addr.s_addr 为IP地址

sin_zero 填充0以保持与struct sockaddr大小一致。

这个数据结构使得使用其中的各个元素更为方便。要注意的是sin_zero应该使用bzero() 或者memset()而设置为全0。另外,一个指向sockaddr_in数据结构的指针可以投射到一个指向数据结构sockaddr的指针,反之亦然。

4.网络字节顺序

一个网络可能由不同的体系结构的CPU组成,这些不同体系的CPU使用的字节顺序不同,有的CPU使用big_endian(大端,在存储器中高字节存储在后),有的CPU使用little_endian(小端,在存储器中高字节存储在前)。因此为了在网络间能够进行数据交换,需要对这些不同的字节顺序进行处理。

下面是几个字节顺序转换函数:

htons():表示“Host to Network Short”,把主机地址字节顺序转向网络字节顺序(对短整型数据操作)。

htonl():表示“Host to Network Long”,把主机地址字节顺序转向网络字节顺序(对长整型数据操作)。

ntohs():表示“Network to Host Short”,把网络字节顺序转向主机地址字节顺序(对短整型数据操作)。

ntohl():表示“Network to Host Short”,把网络字节顺序转向主机地址字节顺序(对长整型数据操作)。

二、SOCKET通信常用API

1.socket(建立一个socket通信) 相关函数 accept,bind,connect,listen

表头文件

#include

#include

定义函数

int socket(int domain,int type,int protocol);

函数说明

socket()用来建立一个新的socket,也就是向系统注册,通知系统建立一通信端口。参数domain 指定使用何种的地址类型,完整的定义在/usr/include/bits/socket.h 内,底下是常见的协议:

PF_UNIX/PF_LOCAL/AF_UNIX/AF_LOCAL UNIX 进程通信协议

PF_INET?AF_INET Ipv4网络协议

PF_INET6/AF_INET6 Ipv6 网络协议

PF_IPX/AF_IPX IPX-Novell协议

PF_NETLINK/AF_NETLINK 核心用户接口装置

PF_X25/AF_X25 ITU-T X.25/ISO-8208 协议

PF_AX25/AF_AX25 业余无线AX.25协议

PF_ATMPVC/AF_ATMPVC 存取原始ATM PVCs

PF_APPLETALK/AF_APPLETALK appletalk(DDP)协议

PF_PACKET/AF_PACKET 初级封包接口

参数

type有下列几种数值:

SOCK_STREAM 提供双向连续且可信赖的数据流,即TCP。支持

OOB 机制,在所有数据传送前必须使用connect()来建立连线状态。

SOCK_DGRAM 使用不连续不可信赖的数据包连接

SOCK_SEQPACKET 提供连续可信赖的数据包连接

SOCK_RAW 提供原始网络协议存取

SOCK_RDM 提供可信赖的数据包连接

SOCK_PACKET 提供和网络驱动程序直接通信。

protocol用来指定socket所使用的传输协议编号,通常此参考不用管它,设为0即可。

返回值

成功则返回socket处理代码,失败返回-1。

错误代码

E

PROTONOSUPPORT 参数domain指定的类型不支持参数type或protocol指定的协议

ENFILE 核心内存不足,无法建立新的socket结构

EMFILE 进程文件表溢出,无法再建立新的socket

EACCESS 权限不足,无法建立type或protocol指定的协议

ENOBUFS/ENOMEM 内存不足

EINVAL 参数domain/type/protocol不合法

2.bind(对socket定位) 相关函数 socket,accept,connect,listen

表头文件

#include

#include

定义函数

int bind(int sockfd,struct sockaddr * my_addr,int addrlen);

函数说明

bind()用来设置给参数sockfd的socket一个名称。

参数

addrlen为sockaddr的结构长度。

返回值

成功则返回0,失败返回-1,错误原因存于errno中。

错误代码

EBADF 参数sockfd 非合法socket处理代码。

EACCESS 权限不足

ENOTSOCK 参数sockfd为一文件描述词,非socket。

3. connect(建立socket连线) 相关函数 socket,bind,listen

表头文件

#include

#include

定义函数

int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);

函数说明

connect()用来将参数sockfd 的socket 连至参数serv_addr 指定的网络地址。结构sockaddr请参考bind()。参数addrlen为sockaddr的结构长度。

返回值

成功则返回0,失败返回-1,错误原因存于errno中。

错误代码

EBADF 参数sockfd 非合法socket处理代码

EFAULT 参数serv_addr指针指向无法存取的内存空间

ENOTSOCK 参数sockfd为一文件描述词,非socket。

EISCONN 参数sockfd的socket已是连线状态

ECONNREFUSED 连线要求被server端拒绝。

ETIMEDOUT 企图连线的操作超过限定时间仍未有响应。

ENETUNREACH 无法传送数据包至指定的主机。

EAFNOSUPPORT sockaddr结构的sa_family不正确。

EALREADY socket为不可阻断且先前的连线操作还未完成。

范例

/* 此程序会连线TCP server,并将键盘输入的字符串传送给server。*/

#include

#include

#include

#include

#include

#include

#include

#define PORT 1234

#define SERVER_IP “127.0.0.1”

int main(void)

{

int s;

struct sockaddr_in addr;

char buffer[256];

if((s = socket(AF_INET,SOCK_STREAM,0))<0)

{

perror(“socket”);

exit(1);

}

/* 填写sockaddr_in结构*/

bzero(&addr,sizeof(addr));

addr.sin_family = AF_INET;

addr.sin_port=htons(PORT);

addr.sin_addr.s_addr = inet_addr(SERVER_IP);

/* 尝试连线*/

if(connect(s,&addr,sizeof(addr))<0)

{

perror(“connect”);

exit(1);

}

/* 接收由server端传来的信息*/

recv(s,buffer,sizeof(buffer),0);

printf(“%s\

”,buffer);

while(1)

{

bzero(buffer,sizeof(buffer));

/* 从标准输入设备取得字符串*/

read(STDIN_FILENO,buffer,sizeof(buffer));

/* 将字符串传给serv

er端*/

if(send(s,buffer,sizeof(buffer),0)<0)

{

perror(“send”);

exit(1);

}

}

}

执行

$ ./connect

Welcome to server!

hi I am client! /*键盘输入*/

/*中断程序*/

4. accept(接受socket连线) 相关函数 socket,bind,listen,connect

表头文件

#include

#include

定义函数

int accept(int s,struct sockaddr * addr,int * addrlen);

函数说明

accept()用来接受参数s的socket连线。参数s的socket必需先经bind()、listen()函数处理过,当有连线进来时 accept()会返回一个新的socket处理代码,往后的数据传送与读取就是经由新的socket处理,而原来参数s的socket能继续使用 accept()来接受新的连线要求。连线成功时,参数addr所指的结构会被系统填入远程主机的地址数据,参数addrlen为scokaddr的结构 长度。关于结构sockaddr的定义请参考bind()。

返回值

成功则返回新的socket处理代码,失败返回-1,错误原因存于errno中。

错误代码

EBADF 参数s 非合法socket处理代码。

EFAULT 参数addr指针指向无法存取的内存空间。

ENOTSOCK 参数s为一文件描述词,非socket。

EOPNOTSUPP 指定的socket并非SOCK_STREAM。

EPERM 防火墙拒绝此连线。

ENOBUFS 系统的缓冲内存不足。

ENOMEM 核心内存不足。

5. listen(等待连接) 相关函数 socket,bind,accept,connect

表头文件

#include

定义函数

int listen(int s,int backlog);

函数说明

listen()用来等待参数s 的socket连线。参数backlog指定同时能处理的最大连接要求,如果连接数目达此上限则client端将收到ECONNREFUSED的错误。 Listen()并未开始接收连线,只是设置socket为listen模式,真正接收client端连线的是accept()。通常listen()会 在socket(),bind()之后调用,接着才调用accept()。

返回值

成功则返回0,失败返回-1,错误原因存于errno

附加说明

listen()只适用SOCK_STREAM或SOCK_SEQPACKET的socket类型。如果socket为AF_INET则参数backlog 最大值可设至128。

错误代码

EBADF 参数sockfd非合法socket处理代码

EACCESS 权限不足

EOPNOTSUPP 指定的socket并未支援listen模式。

范例

#include

#include

#include

#include

#include

#define PORT 1234

#define MAXSOCKFD 10

int main(void)

{

int sockfd,newsockfd,is_connected[MAXSOCKFD],fd;

struct sockaddr_in addr;

int addr_len = sizeof(struct sockaddr_in);

fd_set readfds;

char buffer[256];

char msg[ ] =”Welcome to server!”;

if ((sockfd = socket(AF_INET,SOCK_STREAM,0))<0)

{

perror(“socket”);

exit(1);

}

bzero(&addr,sizeof(addr));

addr.sin_family =AF_INET;

addr.sin_port = htons(PORT);

addr.sin_addr.s_addr = htonl(INADDR_ANY);

if(bind(soc

kfd,&addr,sizeof(addr))<0)

{

perror(“connect”);

exit(1);

}

if(listen(sockfd,3)<0)

{

perror(“listen”);

exit(1);

}

for(fd=0;fdis_connected[fd]=0;

while(1)

{

FD_ZERO(&readfds);

FD_SET(sockfd,&readfds);

for(fd=0;fd{

if(is_connected[fd])

FD_SET(fd,&readfds);

}

if(!select(MAXSOCKFD,&readfds,NULL,NULL,NULL))

continue;

for(fd=0;fd{

if(FD_ISSET(fd,&readfds))

{

if(sockfd = =fd)

{

if((newsockfd = accept (sockfd,&addr,&addr_len))<0)

perror(“accept”);

write(newsockfd,msg,sizeof(msg));

is_connected[newsockfd] =1;

printf(“cnnect from %s\

”,inet_ntoa(addr.sin_addr));

}

else

{

bzero(buffer,sizeof(buffer));

if(read(fd,buffer,sizeof(buffer))<=0)

{

printf(“connect closed.\

”);

is_connected[fd]=0;

close(fd);

}

else

printf(“%s”,buffer);

}

}

}

}

执行

$ ./listen

connect from 127.0.0.1

hi I am client

connected closed.

6. select()

select()系统调用可以使进程检测同时等待的多个I/O设备,当没有设备准备好时,select()阻塞,其中任一设备准备好时,select()就返回。

select()的调用形式为:

#include

#include

int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const struct timeval *timeout);

参数:

select 的第一个参数是文件描述符集中要被检测的比特数,这个值必须至少比待检测的最大文件描述符大1;参数readfds指定了被读监控的文件描述符集;参数 writefds指定了被写监控的文件描述符集;而参数exceptfds指定了被例外条件监控的文件描述符集。

参数timeout起了定时器的作用:到了指定的时间,无论是否有设备准备好,都返回调用。timeval的结构定义如下:

struct timeval{

long tv_sec; //表示几秒

long tv_usec; //表示几微妙

}

timeout取不同的值,该调用就表现不同的性质:

1.timeout为0,调用立即返回;

2.timeout为NULL,select()调用就阻塞,直到知道有文件描述符就绪;

3.timeout为正整数,就是一般的定时器。

select调用返回时,除了那些已经就绪的描述符外,select将清除readfds、writefds和exceptfds中的所有没有就绪的描述符。select的返回值有如下情况:

1.正常情况下返回就绪的文件描述符个数;

2.经过了timeout时长后仍无设备准备好,返回值为0;

3.如果select被某个信号中断,它将返回-1并设置errno为EINTR。

4.如果出错,返回-1并设置相应的errno。

8.send(经socket传送数据) 相关函数 sendto,sendmsg,recv,recvfrom,socket

表头文件

#include

#include

定义函数

int send(int s,const void * msg,int len,unsigned int falgs);

函数说明

send()用来将数据由指定的socket 传给对方主机。参数s为已建立好连接的socket。参数msg指向欲连线的数据内容,参数len则为数据长度。参数flags一般设0,其他数值定义如下

MSG_OOB 传送的数据以out-of-band 送出。

MSG_DONTROUTE 取消路由表查询

MSG_DONTWAIT 设置为不可阻断运作

MSG_NOSIGNAL 此动作不愿被SIGPIPE 信号中断。

返回值

成功则返回实际传送出去的字符数,失败返回-1。错误原因存于errno

错误代码

EBADF 参数s 非合法的socket处理代码。

EFAULT 参数中有一指针指向无法存取的内存空间

ENOTSOCK 参数s为一文件描述词,非socket。

EINTR 被信号所中断。

EAGAIN 此操作会令进程阻断,但参数s的socket为不可阻断。

ENOBUFS 系统的缓冲内存不足

ENOMEM 核心内存不足

EINVAL 传给系统调用的参数不正确。

9.recv(经socket接收数据) 相关函数 recvfrom,recvmsg,send,sendto,socket

表头文件

#include

#include

定义函数

int recv(int s,void *buf,int len,unsigned int flags);

函数说明

recv()用来接收远端主机经指定的socket传来的数据,并把数据存到由参数buf 指向的内存空间,参数len为可接收数据的最大长度。

参数

flags一般设0。其他数值定义如下:

MSG_OOB 接收以out-of-band 送出的数据。

MSG_PEEK 返回来的数据并不会在系统内删除,如果再调用recv()会返回相同的数据内容。

MSG_WAITALL强迫接收到len大小的数据后才能返回,除非有错误或信号产生。

MSG_NOSIGNAL此操作不愿被SIGPIPE信号中断返回值成功则返回接收到的字符数,失败返回-1,错误原因存于errno中。

错误代码

EBADF 参数s非合法的socket处理代码

EFAULT 参数中有一指针指向无法存取的内存空间

ENOTSOCK 参数s为一文件描述词,非socket。

EINTR 被信号所中断

EAGAIN 此动作会令进程阻断,但参数s的socket为不可阻断

ENOBUFS 系统的缓冲内存不足。

ENOMEM 核心内存不足

EINVAL 传给系统调用的参数不正确。

10.read(由已打开的文件读取数据) 相关函数 readdir,write,fcntl,close,lseek,readlink,fread

表头文件

#include

定义函数

ssize_t read(int fd,void * buf ,size_t count);

函数说明

read()会把参数fd 所指的文件传送count个字节到buf指针所指的内存中。若参数count为0,则read()不会有作用并返回0。返回值为实际读取到的字节数,如果返回0,表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动。

附加说明 如果顺利read()会返回实际读到的字节数,最好能将返回值与参数count 作比较,若返回的字节数比要求读取的字节数

少,则有可能读到了文件尾、从管道(pipe)或终端机读取,或者是read()被信号中断了读取动作。当有错误发生时则返回-1,错误代码存入errno中,而文件读写位置则无法预期。

错误代码

EINTR 此调用被信号所中断。

EAGAIN 当使用不可阻断I/O 时(O_NONBLOCK),若无数据可读取则返回此值。

EBADF 参数fd 非有效的文件描述词,或该文件已关闭。

11.write(将数据写入已打开的文件内) 相关函数 open,read,fcntl,close,lseek,sync,fsync,fwrite

表头文件

#include

定义函数

ssize_t write (int fd,const void * buf,size_t count);

函数说明

write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。当然,文件读写位置也会随之移动。

返回值

如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。

错误代码

EINTR 此调用被信号所中断。

EAGAIN 当使用不可阻断I/O 时(O_NONBLOCK),若无数据可读取则返回此值。

EADF 参数fd非有效的文件描述词,或该文件已关闭。

补充:文件描述符

系统提供了4个宏对描述符集进行操作:

#include

#include

void FD_SET(int fd, fd_set *fdset);

void FD_CLR(int fd, fd_set *fdset);

void FD_ISSET(int fd, fd_set *fdset);

void FD_ZERO(fd_set *fdset);

宏FD_SET 设置文件描述符集fdset中对应于文件描述符fd的位(设置为1),宏FD_CLR清除文件描述符集fdset中对应于文件描述符fd的位(设置为 0),宏FD_ZERO清除文件描述符集fdset中的所有位(既把所有位都设置为0)。使用这3个宏在调用select前设置描述符屏蔽位,在调用 select后使用FD_ISSET来检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。

过去,描述符集被作为一个整数位屏蔽码得到实现,但是这种实现对于多于32个的文件描述符将无法工作。描述符集现在通常用整数数组中的位域表示,数组元素的每一位对应一个文件描述符。例如,一个整数占32位,那么整数数组的第一个元素代表文件描述符0到31,数组的第二个元素代表文件描述符32到63,以此类推。宏FD_SET设置整数数组中对应于fd文件描述符的位为1,宏FD_CLR设置整数数组中对应于fd文件描述符的位为0,宏FD_ZERO设置整数数组中的所有位都为0。假设执行如下程序后:

#include

#include

fd_set readset;

FD_ZERO(&readset);

FD_SET(5, &readset);

FD_SET(33, &readset);

再执行如下程序后:

FD_CLR(5, &readset);

通常,操作系统通过宏FD_SETSIZE来声明在一个进程中select所能操作的文件描述符的最大数目。例如:

在4.4BSD的头文件中我们可以看到:

#ifndef FD_SETSIZE

#define FD_SETSIZE 1024

#endif

在红帽Linux的头文件中我们可以看到:

#define __FD_SETSIZE 1024

以及在头文件中我们可以看到:

#include

#define FD_SETSIZE __FD_SETSIZE

既定义FD_SETSIZE为1024,一个整数占4个字节,既32位,那么就是用包含32个元素的整数数组来表示文件描述符集。我们可以在头文件中修改这个值来改变select使用的文件描述符集的大小,但是必须重新编译内核才能使修改后的值有效。当前版本的unix操作系统没有FD_SETSIZE 的最大值,通常只受内存以及系统管理上的。

我们明白了文件描述符集的实现机制之后,就可对其进行灵活运用。(以下程序在红帽Linux 6.0下运行通过,函数fd_isempty用于判断文件描述符集是否为空;函数fd_fetch取出文件描述符集中的所有文件描述符)

#include

#include

#include

#include

struct my_fd_set{

fd_set fs; //定义文件描述符集fs

unsigned int nconnect; //文件描述符集fs中文件描述符的个数

unsigned int nmaxfd; //文件描述符集fs中最大的文件描述符

};

/* 函数fd_isempty用于判断文件描述符集是否为空,为空返回1,不为空则返回0 */

int fd_isempty(struct my_fd_set *pfs)

{

int i;

/* 文件描述符集fd_set是通过整数数组来实现的,所以定义整数数组myset的元素个数为文件描述符集fd_set所占内存空间的字节数除以整数所占内存空间的字节数。

*/

unsigned int myset[sizeof(fd_set) / sizeof(int)];

/* 把文件描述符集pfs->fs 拷贝到数组myset */

memcpy(myset, &pfs->fs, sizeof(fd_set));

for(i = 0; i < sizeof(fd_set) / sizeof(int); i++)

/* 如果myset的某个元素不为0,说明文件描述符集不为空,则函数返回0 */

if (myset[i])

return 0;

return 1; /* 如果myset的所有元素都为0,说明文件描述符集为空,则函数返回1 */

}

/* 函数fd_fetch对文件描述符集进行位操作,把为1的位换算成相应的文件描述符,然后就可对其进行I/O操作 */

void fd_fetch(struct my_fd_set *pfs)

{

struct my_fd_set *tempset; //定义一个临时的结构指针

unsigned int myset[sizeof(fd_set)/sizeof(unsigned int)];

unsigned int i, nbit, nfind, ntemp;

tempset = pfs;

memcpy(myset, &tempset->fs, sizeof(fd_set));

/* 把最大的文件描述符maxfd除以整数所占的位数,得出maxfd在文件描述符集中相应的位对应于整数数组myset的相应元素的下标,目的是为了减少检索的次数 */

nfind = tempset->nmaxfd / (sizeof(int)*8);

for (i = 0; i <= nfind; i++) {

/* 如果数组myset的某个元素为0,说明这个元素所对应的文件描述符集的32位全为0,则继续判断下一元素。*/

if (myset[i] ==

0) continue;

/* 如果数组myset的某个元素不为0,说明这个元素所对应的文件描述符集的32位中有为1的,把myset[i]赋值给临时变量ntemp,对ntemp进行位运算,把为1的位换算成相应的文件描述符 */

ntemp = myset[i];

/* nbit记录整数的二进制位数,对ntemp从低到高位进行&1运算,直到整数的最高位,或直到文件描述符集中文件描述符的个数等于0 */

for (nbit = 0; tempset->nconnect && (nbit < sizeof(int)*8); nbit++) {

if (ntemp & 1) {

/* 如果某位为1,则可得到对应的文件描述符为nbit + 32*I,然后我们可对其进行I/O操作。这里我只是做了简单的显示。*/

printf("i = %d, nbit = %d, The file description is %d\下载本文

显示全文
专题