说明:只供学习交流,转载请注明出处
一,工作流程
使用流套接字实现网络中不同主机间的通信属于典型的服务器/客户机模型,即客服端向服务器发送服务请求,服务器根据该请求提供相应的服务。
为了实现服务器与客户机间的通信,服务器和客户机都必须创建套接字。服务器在创建套接字后,需要指定监听的端口来等待客户机,因此还有绑定端口号的操作。之后,服务器处于监听状态,等待客户机来连接指定端口。当接收到客户机的连接请求后,服务器调用accept函数来建立与客户机的通信。在成功建立通信后,就可以通过read函数或write函数进行通信。
客户端处的流程与服务器相比,简单一些。客户端在创建套接字后,调用connect函数去连接服务器指定的端口。在服务器连接后,客户机和服务器之间就可以通过write函数和read函数实现数据通信了。
小结:
基于TCP-服务器
1. 创建一个socket,用函数socket()
2. 绑定IP地址、端口等信息到socket上,用函数bind()
3. 设置允许的最大连接数,用函数listen()
4. 接收客户端上来的连接,用函数accept()
5. 收发数据,用函数send()和recv(),或者read()和write()
6. 关闭网络连接
基于TCP-客户端
1. 创建一个socket,用函数socket
2. 设置要连接的对方的IP地址和端口
3. 连接服务器,用函数connect()
4. 收发数据,用函数send()和recvread()和write()
5. 关闭网络连接
二,socket函数
socket函数的具体信息如下表所示:
头文件 | #include <sys/types.h> #include <sys/socket.h> | ||
函数原型 | int socket(int domain, int type, int protocol) | ||
返回值 | 成功 | 失败 | 是否设置errno |
创建的socket文件描述符 | -1 | 是 |
说明:socket函数用于创建通信的套接字,并返回该套接字的文件描述符。参数domain指定了通信域,该参数用于选择通信协议族,其取值情况如下表:
名称 | 含义 | 备注 |
PF_UNIX PF_LOCAL | 本地通信 | man 7 UNIX可以获得具体帮助信息 |
PF_INET | IPv4协议 | “man 6 ip”可以获得具体帮助信息 |
PF_INET6 | IPv6协议 | —— |
PF_IPX | Novell公司的协议 | —— |
PF_NETLINK | 与内核间的接口 | Man 7 netlink可以获得具体帮助信息 |
PF_X25 | ITU-T X.25/ISO-8208 | Man 7 x25可以获得具体帮助信息 |
PF_AX25 | 无线AX.25协议 | —— |
PF_ATMPVC | 访问原始ATM的PVC | —— |
PF_APPLETALK | 苹果公司的Appletalk协议 | Man 7 ddp可以获得具体帮助信息 |
PF_PACKET | 底层包接口 | Man 7 packet获得具体帮助信息 |
参数type用于指定套接字类型。可以取以下值:
SOCK_STREAM:提供有序、可靠、双向及基于连接的字节流。支持带外传输机制。
SOCK_DGRAM:支持数据报。
SOCK_SEQPACKET:提供有序、可靠、双向基于连接的数据报通信。
SOCK_RAW:提供对原始网络协议的访问。
SOCK_RDM:提供可靠的数据报层,但是不保证有序性。
SOCK_PACKET:该参数已经被废除。
流套接字(SOCK_STREAM)与管道类似,是一种全双工的比特流。流套接字在发送或数据接收前必须处于连接状态。实现流套接字的通信协议保证了传输数据不会丢失。
protocol指定应用程序所使用的通信协议。如果在给定的协议族中有需要使用的协议,这个参数填0就可以了
错误信息:
EACCES:创建指定类型的套接字失败。
EAFNOSUPPORT:不支持指定的地址族。
EINVAL:未知协议或未知协议族。
EMFILE:进程文件表溢出。
ENFILE:达到打开文件的系统限制。
ENOBUFS或ENOMEM:内存不足。
EPROTONOSUPPORT:指定的协议类型在该域中不支持。
三,bind函数
bind函数用于将套接字与指定端口相连,其具体信息如下:
头文件 | #include <sys/types.h> #include <sys/socket.h> | ||
函数原型 | int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen); | ||
返回值 | 成功 | 失败 | 是否设置errno |
0 | -1 | 是 |
说明:当调用socket函数创建套接字后,该套接字并没有与本地地址和端口等信息相连,bind函数完成这些工作。bind函数中的sockfd参数为调用socket函数后返回的文件描述符。my_addr参数为指向sockaddr结构体的指针(该结构体中保存有端口和IP地址信息)。addrlen参数为结构体sockaddr的长度。
错误信息:
EACCES:地址受到保护,用户非超级用户。
EADDRINUSE:指定的地址已经在使用。
EBADF:sockfd参数为非法的文件描述符。
EINVAL:socket已经和地址绑定。
ENOTSOCK:参数sockfd为文件描述符。
四,listen函数
服务器必须等待客户的连接请求,listen函数用于实现等待功能,该函数的具体信息如下表:
头文件 | #include <sys/socket.h> | ||
函数原型 | Int listen(int sockfd, int backlog); | ||
返回值 | 成功 | 失败 | 是否设置errno |
0 | -1 | 是 |
说明:listen函数中,参数sockfd为调用socket函数获得的套接字的文件描述符。backlog参数为提出连接请求后,在服务器接收该连接请求时的等待队列中的连接数。默认情况,该值为20.
系统调用listen只用于套接字类型为SOCK_STREAM或SOCK_SEQPACKET的场合。
错误信息:
EADDRINUSE:另一个socket也在监听同一个端口。
EBADF:参数sockfd为非法的文件描述符。
ENOTSOCK:参数sockfd不是文件描述符。
EOPNOTSUPP:套接字类型不支持listen函数。
五,accept函数
处于监听状态的服务器在获得客户机的连接请求后,会将其放置在等待队列中。当系统空闲时,将接受客户机的连接请求。接收客户机的连接请求使用accept函数,该函数的具体信息如下表:
头文件 | #include <sys/types.h> #include <sys/socket.h> | ||
函数原型 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); | ||
返回值 | 成功 | 失败 | 是否设置errno |
返回新的套接字文件描述符 | -1 | 是 |
说明:accept函数用于面向连接类型的套接字(SOCK_STREAM或SOCK_SEQPACKET)。accept函数将从连接请求队列中获得连接信息,创建新的套接字,并返回该套接字的文件描述符。新创建的套接字用于服务器与客户机的通信,而原来的套接字任然处于监听状态。
accept函数的sockfd参数为监听的套接字描述符。addr参数为指向结构体sockaddr的指针,用于接收请求连接的客户端的地址。addrlen为addr参数指向的内存空间的长度。
错误信息:
EAGAIN:套接字处于非阻塞状态,当前没有连接请求。
EBADF:非法的文件描述符。
ECONNABORTED:连接中断。
EINTR:系统调用被信号中断。
EINVAL:套接字没有处于监听状态,或非法的addrlen参数。
EMFILE:达到进程打开文件描述符限制。
ENFILE:达到打开文件数限制。
ENOTSOCK:文件描述符为文件的描述符。
EOPNOTSUPP:套接字类型不是SOCK_STREAM。
六,connect函数
对于客户机而言,要与服务器进行通信,需要向服务器发出连接请求。connect函数用于完成这项功能。该函数的具体信息如下表所示:
头文件 | #include <sys/types.h> #include <sys/socket.h> | ||
函数原型 | int connect(int socket, const struct sockaddr *serv_addr, socklen_t addrlen); | ||
返回值 | 成功 | 失败 | 是否设置errno |
0 | -1 | 是 |
说明:sockfd为连接至服务器的套接字;ser_addr用于指定服务器的地址;addrlen为第二个参数的大小。
如果参数sockfd的类型为SOCK_DGRAM,ser_addr参数为数据报发往的地址,且将只接收该地址的数据报。如果sockfd的类型为SOCK_STREAM或SOCK_SEQPACKET,调用该函数将连接ser_addr中服务器地址。
错误信息:
EACCES,EPERM:用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致失败。
EADDRINUSE:本地地址处于使用状态。
EAFNOSUPPORT:参数ser_add中的地址非法地址。
EAGAIN:没有足够空闲的地址端口。
EALREADY:套接字为非法阻塞套接字,并且原来的连接请求还未完成。
EBADF:非法的文件描述符。
ECONNREFUSED:远程地址并没有处于监听状态。
EFAULT:指向套接字结构体的地址非法。
EINPROGRESS:套接字为非阻塞套接字,且连接请求没有立即完成。
EINTR:系统调用的执行由于捕获中断而中止。
EISCONN:已经连接到该套接字。
ENETUNREACH:网络不可到达。
ENOTSOCK:文件描述符不与套接字相关。
ETIMEDOUT:连接超时。
七,发送和接收数据
当服务器与客户机之间成功建立连接后,可以调用read和write函数来实现对套接字的读写,以实现网络中不同主机间通信。Linux系统还提供了send和recv函数,用于实现与read和write函数相同的功能。而且send和recv的功能要比read函数和write函数更为全面。send函数的具体信息如下表:
头文件 | #include <sys/types.h> #include <sys/socket.h> | ||
函数原型 | ssize_t send(int s, const void *buf, size_t len, int flags); | ||
返回值 | 成功 | 失败 | 是否设置errno |
返回实际发送的数据字节数 | -1 | 是 |
send函数用于将信息发送到指定的套接字文件描述符中。该函数只能用于已经建立连接的socket通信中,即只用于面向连接的通信中。参数s为要发送数据的套接字文件描述符。buf参数为指向要发送数据的指针。len为要发送数据的长度。flag参数可以包含如下的参数。
MSG_CONFIRM(Linux 2.3以上的内核版本支持):通知数据链路层发生了转发,且得到了通信另一段的回应。如果链路层没有得到回应,将使用ARP或其他协议来探测网络上的主机。该参数只用于SOCK_DGRAM和SOCK_RAW类型的套接字。
MSG_DONTROUTE:不通过网关发送数据,只将数据发送到同一子网中的计算机。该参数通常用于诊断或路由程序中,只用于路由的协议族中,包套接字不能使用该参数。
MSG_DONTWAIT:使用非阻塞操作。如果操作将阻塞,并返回EAGAIN错误。
MSG_EOR:结束记录(当套接字类型是SOCK_SEQPACKET时使用)。
MSG_MODE(Linux 2.4.4以上内核版本支持):调用者有更多的数据要发送。
MSG_OOB:通过套接字发送带外数据(套接字需要支持这一行为,例如,使用SOCK_STREAM类型的套接字)。
write函数与send函数在flag为0时的功能相同。
错误信息:
下面列出send函数常见的错误信息。
EBADF:非法的文件描述。
ECONNRESET:连接重置。
EDESTADDRREQ:在套接字操作中没有指定目标地址。
EFAULT:参数指向了非法的地址空间。
EINTR:数据发送前,捕获到信号。
EINVAL:非法参数。
ENOTSOCK:参数非套接字文件描述符。
ENOMEM:内存不足。
recv函数可以实现从指定套接字中读取发送来的消息,该函数的具体信息如下表:
头文件 | #include <sys/types.h> #include <sys/socket.h> | ||
函数原型 | ssize_t recv(int s, void *buf, size_t len, int flags); | ||
返回值 | 成功 | 失败 | 是否设置errno |
|
|
|
说明:recv函数用于从指定套接字中获取发送的消息。与send函数一样,该函数只能用于已经建立连接的socket通信中,即只用于面向连接的通信中。参数s为要读取信息的套接字文件描述符。buf参数为指向要保存数据缓冲区的指针。len为该缓存的最大长度。
参数flags可以包含如下标志:
MSG_DONTWAIT:使用非阻塞操作。如果操作将阻塞,将返回EAGAIN错误。
MSG_OOB:通过套接字发送带外数据(套接字需要支持这一行为,例如使用SOCK_STREAM类型的套接字)。
MSG_PEEK:该标志表示从接收队列的开始处查看数据,而不从缓冲区中删除数据。
MSG_TRUNC:返回包的真实长度,即使该长度超出了传递的缓存长度。该标志只用于流套接字。
MSG_WAITALL:该标志将使得操作处于阻塞状态,直到获得全部数据。
当flags参数为0时,recv函数等同与read函数的功能。
错误信息:
EAGAIN:在接收到数据前,接收操作处于阻塞或直至超时。
EBADF:非法的文件描述符。
ECONNABORTED:远程主机拒绝网络连接。
EFAULT:指向接收数据的缓冲区指针指向了非法地址空间。
EINTR:系统调用被信号中断。
EINVAL:非法参数。
ENOTCONN:套接字使用了面向连接的协议,但是并没有建立连接。
ENOTSOCK:文件描述符为文件的文件描述符。
八,关闭套接字
在完成通信后,可以使用close函数或shutdown函数来关闭套接字。close函数的调用形式为:
close(sockfd);
实例:
Server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#define UNIX_DOMAIN "/tmp/UNIX.domain"
int main(void)
{
socklen_t clt_addr_len;
int listen_fd;
int com_fd;
int ret;
int i;
static char recv_buf[1024];
int len;
struct sockaddr_un clt_addr;
struct sockaddr_un srv_addr;
listen_fd = socket(PF_UNIX, SOCK_STREAM, 0);
if (listen_fd < 0)
{
perror("Cannot create listening socket");
return (1);
}
srv_addr.sun_family = AF_UNIX;
strncpy(srv_addr.sun_path, UNIX_DOMAIN, sizeof(srv_addr.sun_path)-1);
unlink(UNIX_DOMAIN);
ret = bind(listen_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
if (ret == -1)
{
perror("Cannot bind server socket");
close(listen_fd);
unlink(UNIX_DOMAIN);
return (1);
}
ret = listen(listen_fd, 1);
if (ret == -1)
{
perror("Cannot listen the client connect request");
close(listen_fd);
unlink(UNIX_DOMAIN);
return (1);
}
len = sizeof(clt_addr);
com_fd = accept(listen_fd, (struct sockaddr*)&clt_addr, &len);
if (com_fd < 0)
{
perror("Cannot accept client connect request");
close(listen_fd);
unlink(UNIX_DOMAIN);
return (1);
}
printf("\n=======info=======\n");
for (i = 0; i < 4; i++)
{
memset(recv_buf, 0, 1024);
int num = read(com_fd, recv_buf, sizeof(recv_buf));
printf("Message from client (%d) : %s\n", num, recv_buf);
write(com_fd, "hello linux", sizeof("hello linux"));
}
close(com_fd);
close(listen_fd);
unlink(UNIX_DOMAIN);
return (0);
}
Client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#define UNIX_DOMAIN "/tmp/UNIX.domain"
int main(void)
{
int connect_fd;
int ret;
char snd_buf[1024] = {'\0'};
int i;
static struct sockaddr_un srv_addr;
connect_fd = socket(PF_UNIX, SOCK_STREAM, 0);
if ( connect_fd < 0 )
{
perror("Cannot create communication socket");
return (1);
}
srv_addr.sun_family = AF_UNIX;
strcpy(srv_addr.sun_path, UNIX_DOMAIN);
ret = connect(connect_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
if (ret == -1)
{
perror("Cannot connect to the server");
close(connect_fd);
return (1);
}
memset(snd_buf, 0, 1024);
strcpy(snd_buf, "Message from client");
char recv_buf[024];
for (i = 0; i < 4; i++)
{
write(connect_fd, snd_buf, sizeof(snd_buf));
read(connect_fd, recv_buf, 1024 );
printf("Receive from server: %s\n", recv_buf);
}
close(connect_fd);
return(0);
}
运行结果:
实例2:
Server-tcp.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <netdb.h>
//捕获子进程退出信号,在退出时给出提示信息
void sig_handler(int signo)
{
pid_t pid;
int stat;
pid = waitpid(-1, &stat, WNOHANG);
while (pid > 0)
{
printf("Child process terminated (PID : %ld)\n", (long)getpid());
pid = waitpid(-1, &stat, WNOHANG);
}
return ;
}
int main(int argc, char *argv[])
{
socklen_t clt_addr_len;
int listen_fd;
int com_fd;
int ret;
int i;
static char recv_buf[1024];
int len;
int port;
pid_t pid;
struct sockaddr_in clt_addr;
struct sockaddr_in srv_addr;
//服务器运行时要给出端口信息,该端口为监听端口
if (argc != 2)
{
printf("Usage: %s port\n", argv[0]);
return (1);
}
port = atoi(argv[1]);
if (signal(SIGCHLD, sig_handler) < 0)
{
perror("Cannot set the signal");
return (1);
}
listen_fd = socket(PF_INET, SOCK_STREAM, 0);
if (listen_fd < 0)
{
perror("Cannot create listening socket");
return (1);
}
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
srv_addr.sin_port = htons(port);
ret = bind(listen_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
if (ret == -1)
{
perror("Cannot bind server socket");
close(listen_fd);
return (1);
}
ret = listen(listen_fd, 5);
if (ret == -1)
{
perror("Cannot listen the client connect request");
close(listen_fd);
return (1);
}
//对每个连接来的客户端创建一个进程,单独与其进行通信
//首先调用read函数读取客户端发送来的信息
//将其转换成大写后发送回客户端
//当输入“@”时,程序退出
while ( 1 )
{
len = sizeof(clt_addr);
com_fd = accept(listen_fd, (struct sockaddr*)&clt_addr, &len);
if (com_fd < 0)
{
if (errno == EINTR)
{
continue;
}
else
{
perror("Cannot accept client connect request");
close(listen_fd);
return (1);
}
}
pid = fork();
if (pid < 0)
{
perror("Cannot create the child process");
close(listen_fd);
return (1);
}
else if(pid == 0)
{
while ((len = read(com_fd, recv_buf, 1024))>0)
{
printf("Message from client(%d): %s\n", len, recv_buf);
if (recv_buf[0] == '@')
{
break;
}
for (i = 0; i < len; i++)
{
recv_buf[i] = toupper(recv_buf[i]);
}
write(com_fd, recv_buf, len);
memset(&recv_buf, 0, 1024);
}
close(com_fd);
return (0);
}
else
{
close(com_fd);
}
}
return (0);
}
Client-tcp.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netdb.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int connect_fd;
int ret;
char snd_buf[1024] = {'\0'};
int i;
int port;
int len;
static struct sockaddr_in srv_addr;
//客户端运行需要给出具体的连接地址和端口
if (argc != 3)
{
printf("Usage: %s server_ip_address port\n", argv[0]);
return (1);
}
port = atoi(argv[2]);
connect_fd = socket(PF_INET, SOCK_STREAM, 0);
if (connect_fd < 0)
{
perror("Cannot create communication socket");
return (1);
}
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = inet_addr(argv[1]);
srv_addr.sin_port = htons(port);
//连接指定的服务器
ret = connect(connect_fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
if (ret == -1)
{
perror("Cannot connect to the server");
close(connect_fd);
return (1);
}
memset(snd_buf, 0, 1024);
//用户输入信息后,程序将输入的信息通过套接字发送给服务器
//然后调用read函数从服务器中读取发送来的消息
//当输入“@”时,程序退出
while ( 1 )
{
write(STDOUT_FILENO, "input message:", 14);
len = read(STDIN_FILENO, snd_buf, 1024);
if (len > 0)
{
write(connect_fd, snd_buf, len);
}
memset(&snd_buf, 0, 1024);
len = read(connect_fd, snd_buf, len);
if (len > 0)
{
printf("Message from server: %s\n", snd_buf);
}
if (snd_buf[0] == '@')
{
break;
}
}
close(connect_fd);
return (0);
}
运行结果: