目录
一、回顾读写api以及基础api
数据读写api
TCP数据读写
对文件的读写操作read和 write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:
recv()
ssize_t recv(int sockfd,void *buf,size_t len,int flags);
recv读取sockfd 上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags参数通常设置为0。
成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次调用recv,才能读取到完整的数据。recv返回0表示对方已经关闭连接了。recv出错时返回-1并设置errno。
send()
ssize_t send(int sockfd,const void *buf,size_t len,int flags);
send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度,失败则返回-1并设置errno。
UDP数据读写
另外,这两个函数也可以用于面向连接(STREAM)的socket数据读写,只需要把最后两个参数设置为NULL。
因为UDP通信没有连接的概念,所以我们每次读/写数据都需要 获取发送端(读)/指定接收端(写) 的socket地址和长度。
除此之外,返回值与其它参数的含义均与
recv()
和send()
相同。
recvfrom()
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr* src_addr,socklen_t* addrlen);
recvfrom调用会获取数据传输端的地址,并填充到clnt_adr(因此serv端知道是从哪个客户端接收的数据),正是利用该地址将接收的数据逆向传输 。
读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小。因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen则指定该地址的长度
sendto()
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。dest_addr指定接收端的socket地址,addrlen指定该地址的长度
基础api accept()
accept函数用于从Accepte队列中pop出一个已完成的连接,若Accept队列为空,则accept函数所在的进程阻塞。
需要注意的是,accept函数从listen监听队列中(取出)接收一个连接,而不论该连接处于何种状态(即使客户端网络异常掉线),更不关心网络状况的变化。
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
- p1: 监听socket(门卫)
- p2:
addr
is a pointer to a sockaddr structure. This structure is filled in with the address of the peer socket (accept调用时会将其填充为客户端socket地址,大小由参数addrlen指出//传出参数 - p3:
addrlen
is a value-result argument(传入传出参数): the caller must initialize it to contain the size of the structure pointed to by addr; on return it will contain the actual size of the peer address(传入的是sockaddr结构体的大小,传出的是客户端socket地址的实际大小。accept调用后就被填入客户端socket地址结构体中) - 返回值:成功返回创建的新的连接socket,失败返回-1并设置errno
我们把 执行过listen调用、处于LISTEN状态 的socket称为
监听socket
,而所有处于ESTABLISHED
状态的socket则称为连接socket
.
二、实现TCP回声服务器端/客户端
echo_serv.c
#include <stdio.h> // Standard input/output functions
#include <stdlib.h> // Standard library functions
#include <string.h> // String handling functions
#include <unistd.h> // Unix standard functions
#include <arpa/inet.h> // IP address conversion functions
#include <sys/socket.h> // Socket functions
#define BUF_SIZE 1024 // Define a constant for the buffer size
void error_handling(char* message); // Function prototype for error handling
int main(int argc, char* argv[]) {
//argv[0] is the program name,argv[1] is port number
int serv_sock, clnt_sock;
//serv_sock: server socket(gatekeeper),
//clnt_sock: connection socket to communicate with the client
char message[BUF_SIZE];
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz; //initialize clnt_adr_sz
if(argc != 2) { //Two parameters are required, one is the program name, and the other one is port number
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// Creates a (gatekeeper) socket.
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1) error_handling("socket() error");
//Initialize the server address structure (gatekeeper)
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
//在调用bind之前设置socket选项SO_REUSEADDR,
//表示允许地址重用, 否则close之后会有一个WAIT_TIME状态,使得该ip和端口仍>被占用,产生bind() error.
int on = 1;
if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int)) < 0) error_handling("setsockopt() error");
// Bind the (gatekeeper) socket, also listening socket later.
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error");
//Listen for incoming connections,set backlog queue of size 5.
if(listen(serv_sock, 5) == -1) error_handling("listen() error");
int i, str_len;
clnt_adr_sz = sizeof(clnt_adr); //initialize clnt_adr_sz
// Accept up to 5 client connections
for (i = 0; i < 5; ++i) {
// 取出连接,并返回与对应客户端进行连接的连接socket
clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_adr, &clnt_adr_sz);
if(clnt_sock == -1) error_handling("accept() error");
else printf("the %dth connected client \n", i + 1);
// Read data from the client and write it back
while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
write(clnt_sock, message, str_len);
close(clnt_sock); //关闭与客户端通信的连接socket
}
close(serv_sock); // Close the server socket (gatekeeper)
return 0;
}
void error_handling(char* message) {
perror(message); // 打印错误信息
exit(1); // 退出程序
}
echo_clnt.c
#include <stdio.h> // Standard input/output functions
#include <stdlib.h> // Standard library functions
#include <string.h> // String handling functions
#include <unistd.h> // Unix standard functions
#include <arpa/inet.h> // IP address conversion functions
#include <sys/socket.h> // Socket functions
#define BUF_SIZE 1024 // Define a constant for the buffer size
void error_handling(char* message); // Function prototype for error handling
int main(int argc, char* argv[]) {
//argv[0] is the program name,argv[1] and argv[2] are the IP and port for the target server socket (gatekeeper Socket)
int clnt_sock;
char message[BUF_SIZE]; //Buffer to hold messages
struct sockaddr_in serv_adr; //Server address structure
// Check if the correct number of command line arguments is provided
if(argc != 3) { //Three parameters are required, one is the program name, and the other two are the IP and port number
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
//Creates a TCP socket.
clnt_sock = socket(PF_INET, SOCK_STREAM, 0);
if(clnt_sock == -1) error_handling("socket() error");
//Initialize the server address structure
memset(&serv_adr, 0, sizeof(serv_adr)); //Clears the structure for server address.
serv_adr.sin_family = AF_INET; //Sets the address family to IPv4
serv_adr.sin_addr.s_addr = inet_addr(argv[1]); //Sets the server IP address
serv_adr.sin_port = htons(atoi(argv[2])); //Sets the server port.
// Connects to the server,and connect() will also assign address info to client socket
if(connect(clnt_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) error_handling("connect() error");
else puts("Connected!");
int str_len, recv_len, recv_cnt;
// Main loop for communication:
while (1) {
fputs("Input message(Q to quit): ", stdout);//standard output
fgets(message, BUF_SIZE, stdin);//User input is read with fgets and sent to the server with write.
//communication is terminated when the user enters "q" or "Q"
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break;
// Send the message to the server
str_len = write(clnt_sock, message, strlen(message)); //提前确定之后要接收的数据的大小
recv_len = 0; //read调用的循环变量
while (recv_len < str_len) { //只要没接收完就一直循环
recv_cnt = read(clnt_sock, &message[recv_len], BUF_SIZE - 1);
if (recv_cnt == -1) error_handling("read() error ! ");
recv_len += recv_cnt;
}
message[recv_len] = 0;
printf("Message from server: %s", message);
}
close(clnt_sock);
return 0;
}
// Function definition for error handling
void error_handling(char* message) {
perror(message); // print error info
exit(1); // exit the program
}
运行结果
# 服务器端
[root@ECSocar ch04]#gcc echo_serv.c -o echo_serv
[root@ECSocar ch04]#./echo_serv 9190
the 1th connected client
the 2th connected client
# 客户端
[root@ECSocar ch04]#gcc echo_clnt.c -o echo_clnt
[root@ECSocar ch04]#./echo_clnt 127.0.0.1 9190
Connected!
Input message(Q to quit): hi
Message from server: hi
Input message(Q to quit): q
[root@ECSocar ch04]# ./echo_clnt 127.0.0.1 9190
Connected!
Input message(Q to quit): q
三、实现UDP回声服务器/客户端
uecho_serv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc, char* argv[]) {
int serv_sock;
char message[BUF_SIZE]; //Buffer for storing received and to-be-sent messages
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if(argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// Creates a UDP socket.
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(serv_sock == -1) error_handling("UDP socket() creation error");
// Initialize the server address structure
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
// Set socket option SO_REUSEADDR to allow the reuse of the address.
int on = 1;
if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int)) < 0) error_handling("setsockopt() error");
// Bind the socket to the specified address and port.
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error");
int str_len;
// this UDP server as it is designed to run indefinitely,
// Enter an infinite loop to continuously receive and send data.
while (1) {
clnt_adr_sz = sizeof(clnt_adr); //initialize clnt_adr_sz
// Use recvfrom to receive data from any client (clnt_adr is populated with client's address).
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
//recvfrom调用会获取数据传输端的地址,并填充到clnt_adr(因此serv端知道是从哪个客户端接收的数据),正是利用该地址将接收的数据逆向传输
// Send the received data back to the same client
sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);
}
//没有break,也就是说是无限循环的,因此close()不会执行,没有意义
close(serv_sock);
return 0;
}
void error_handling(char* message) {
perror(message);
exit(1);
}
uecho_clnt.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc, char* argv[]) {
int clnt_sock;
char message[BUF_SIZE];
struct sockaddr_in serv_adr, from_adr;
socklen_t src_adr_sz;//接收数据时,数据发送端的socket地址的长度
if(argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
clnt_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(clnt_sock == -1) error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
int str_len;
while (1) {
fputs("Insert message(Q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
//communication is terminated when the user enters "q" or "Q"
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break;
// Send the message to the server
sendto(clnt_sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
// Receive a response from src
src_adr_sz = sizeof(src_adr);
str_len = recvfrom(clnt_sock, message, BUF_SIZE , 0, (struct sockaddr*)&src_adr, &src_adr_sz));
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(clnt_sock);
return 0;
}
void error_handling(char* message) {
perror(message);
exit(1);
}
运行结果
和上面TCP的几乎一样
# 服务器端
[root@ECSocar ch06]# gcc uecho_serv.c -o uecho_serv
[root@ECSocar ch06]# ./uecho_serv 9190
^C
# 客户端
[root@ECSocar ch06]# gcc uecho_clnt.c -o uecho_clnt
[root@ECSocar ch06]# ./uecho_clnt 127.0.0.1 9190
Insert message(Q to quit): hi,nihaoya
Message from server: hi,nihaoya
Insert message(Q to quit): how are you?
Message from server: how are you?
Insert message(Q to quit): q
四、TCP_serv & UDP_serv的差异
如有对函数理解不清晰的,请回到第一部分
将中间部分放在一起对比能发现:
-
tcp_serv只对
clnt_adr_sz
初始化一次,且是在循环前进行;而udp_serv每一次传输数据前都需要初始化clnt_adr_sz
,且是在循环内部一开始就进行。书上的例子是这样写的,但是私以为这并不是TCP_serv和UDP_serv的差异,tcp_serv也应该在for内部每次accept调用前对clnt_ad_sz
进行一次初始化。原因是:因为accept的第三个参数addrlen是value-result argument(传入传出参数),每次传入的都应该是sockaddr结构体的大小,传出客户端地址的实际大小(accept调用会修改
clnt_adr_sz
的值)。如果与下一个客户端进行连接之前不重新初始化,那么再调用accept时传入的
clnt_adr_sz
就是上一个客户端地址的实际大小,可能会小于我们要传出(返回)的、当前这个客户端的地址的实际大小,导致返回的客户端地址信息被截断。 -
TCP连接需要
listen() + accept()
先listen监听连接然后accept从全连接队列中取出连接,也就是需要【建立连接】的这个过程,而UDP不需要建立连接就可以直接通信。 -
TCP接收数据时不需要指定客户端地址和地址长度,而UDP需要指定。且tcp_serv是直接从 前面调用accept()时取得的客户端socket(
clnt_sock
) 读取数据,而udp_serv是 直接从serv_sock
中读取数据,在recvfrom()调用的过程中取得客户端socket。 -
传输数据和接收数据差不多同理,就不赘述了
除了上面所展示的差异之外,TCP和UDP的实现还有很多差异,这些差异都是源于
【TCP为应用层提供可靠的、面向连接的、基于流的服务,而UDP为应用层提供不可靠的、无连接的、基于数据报的服务】。之后我们会再详细演示其它的一些差异。