網絡編程 05_使用 TCP 進行讀寫
目標
使用創建的套接字收發數據
一、發送數據
常用函數
send 函數
ssize_t send(int socketfd, const void *buffer, size_t size, int flags)
size_t:無符號
ssize_t:有符號
發送緩衝區
在 TCP 三次握手成功,建立連接後,操作系統會爲每個連接創建對應的發送緩衝區
應用程序調用 write 函數,實際是把數據從應用程序中拷貝到操作系統內核的發送緩衝區中,並不一定是把數據通過套接字寫出去
有了發送緩衝區,那麼存在以下情況:
- 操作系統內核的發送緩衝區足夠大,可以容納這份數據,此時程序從 write 調用中退出,返回寫入的字節數等於應用程序的數據大小
- 操作系統的發送緩衝區不足以容納應用程序數據,應用程序中數據可能發送完了,也可能還沒發送完(需要等到發送緩衝區空出位置,應用程序中數據再繼續發送到發送緩衝區)
第二種情況下,操作系統內核並不返回,也不報錯,而是應用程序被阻塞,即應用程序在 write 調用處停下,不直接返回
write 函數阻塞返回時機
大部分 UNIX 操作系統做法是一直等到可以把應用程序數據完全放到操作系統內核的發送緩衝區中,再從系統調用中返回
二、讀取數據
read 函數
ssize_t read(int socketfd, void *buffer, size_t size)
參數
- socketfd:套接字描述字
- buffer:將讀到的結構緩存起來
- size:讀到的字節數
返回值
- 0:EOF,對端關閉連接
- -1:出錯
三、TCP 通信例子
服務端
tcpserver.c
#include "common.h"
// 一次讀取 size 個字節
size_t readn(int fd, void *buffer, size_t size) {
char *buffer_pointer = buffer;
int length = size;
while (length > 0) {
int result = read(fd, buffer_pointer, length);
if (result < 0) {
if (errno == EINTR) {
continue; // 若爲非阻塞,這裏需再次調用 read
} else {
return (-1);
}
} else if (result == 0) {
break; // EOF,套接字關閉
}
length -= result;
buffer_pointer += result;
}
return (size - length); // 實際讀到的字節數
}
void read_data(int sockfd) {
ssize_t n; // 讀到的字節數
char buf[1024]; // 一次讀的最大字節數
int time = 0;
for (;;) {
fprintf(stdout, "block in read\n");
if ((n = readn(sockfd, buf, sizeof(buf))) == 0)
{
return;
}
time++;
fprintf(stdout, "1K read for %d\n", time);
usleep(1000); // 單位:微秒,這裏是 1000μs = 1ms
}
}
int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clilen; // 指定 accept 函數第三個參數
struct sockaddr_in cliaddr, servaddr;
listenfd = socket(PF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(12345);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 1024);
for (;;) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
read_data(connfd);
close(connfd);
}
}
客戶端
tcpclient.c
#include "common.h"
#define MESSAGE_SIZE 10240000
void send_data(int sockfd) {
char *query;
query = malloc(MESSAGE_SIZE + 1);
for (int i = 0; i < MESSAGE_SIZE; i++) {
query[i] = 'a';
}
query[MESSAGE_SIZE] = '\0';
const char *cp;
cp = query;
size_t remaining = strlen(query);
while (remaining) {
int n_written = send(sockfd, cp, remaining, 0);
fprintf(stdout, "send into buffer %d\n", n_written);
if (n_written <= 0) {
error(1, errno, "send failed");
return;
}
remaining -= n_written;
cp += n_written;
}
return;
}
int main(int argc, char **argv) {
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2) {
error(1, 0, "usage: tcpclient <IPaddress>");
}
sockfd = socket(PF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(12345);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
int connect_rt = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (connect_rt < 0) {
error(1, errno, "connect failed");
}
send_data(sockfd);
close(0);
}
頭文件 common.h
#ifndef CHAP_05_COMMON_H
#define CHAP_05_COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h> /* basic socket definitions */
#include <netinet/in.h> /* sockaddr_in{} and other Internet defns */
#include <arpa/inet.h> /* inet(3) functions */
#include <errno.h>
#include <unistd.h>
void error(int status, int err, char *fmt, ...);
#endif //CHAP_05_COMMON_H
代碼組成:
編譯
gcc -o tcpclient tcpclient.c
gcc -o tcpserver tcpserver.c
執行
① 先運行服務端
./tcpserver
② 再運行客戶端
./tcpclient 127.0.0.1
四、緩衝區實驗
實驗一:觀察客戶端的發送行爲
客戶端發送了一個很大的字節流,程序運行後,服務端不斷在屏幕上打印讀到的字節流,客戶端直到最後所有的字節流發送完畢纔打印 send into buffer 10240000
實驗一中間某個時刻(左:服務端;右:客戶端)
實驗一結束後(左:服務端;右:客戶端)
說明客戶端在 send 函數在返回前一直是阻塞的,即阻塞式套接字最終發送返回的實際寫入字節數和請求字節數是相等的
實驗二:讓服務端處理變慢
將服務端代碼 tcpserver.c 中的休眠時間調大一點
usleep(5000); // 從 1000μs 改爲 5000μs(1ms -> 5ms)
將客戶端代碼 tcpclient.c 中的發送字節數調小一點
#define MESSAGE_SIZE 1024000 // 從 10240000 改爲 1024000
然後重新編譯
gcc -o tcpclient tcpclient.c
gcc -o tcpserver tcpserver.c
再執行
① 先運行服務端
./tcpserver
② 再運行客戶端
./tcpclient 127.0.0.1
實驗二中間某個時刻(左:服務端;右:客戶端)
實驗二結束後(左:服務端;右:客戶端)
這次,客戶端很快打印出了 send into buffer 10240000
,但服務端程序還在屏幕上不斷打印讀到的數據
五、小結
發送成功僅僅表示的是數據拷貝到了發送緩衝區中,並不意味着連接對端已經收到所有的數據
- 對於 send 函數,返回成功僅僅表示數據寫到發送緩衝區成功,並不表示對端已經成功收到
- 對於 read 函數,需要循環讀取數據,並且要考慮 EOF 等異常條件
補充:CMake 管理當前項目
① 先給出代碼組成:
-CMakeLists.txt
-include:存放頭文件
-src:存放源代碼
CMakeLists.txt
# CMake 最低版本
cmake_minimum_required(VERSION 3.10)
# 項目名稱
PROJECT(chap_05)
# 指定生成的可執行文件(目標二進制文件)的位置
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
# 設置頭文件查找路徑
include_directories(${CMAKE_SOURCE_DIR}/include)
# 添加子目錄去構建
ADD_SUBDIRECTORY(src)
include 目錄:include/common.h(common.h 上面有)
src 目錄(tcpclient.c、tcpserver.c 上面有)
src/CmakeLists.txt
add_executable(tcpclient tcpclient.c)
target_link_libraries(tcpclient)
add_executable(tcpserver tcpserver.c)
target_link_libraries(tcpserver)
② 創建 build 目錄
③ 外部編譯(生成的目標二進制文件目錄,對應 cmake 執行的目錄)
cd build
cmake ..
再 make 一下(生成的可執行文件存放目錄,由上面 CMakeLists.txt 中 SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
指令指定)
make
生成的可執行文件在 build/bin 目錄下