HelloWord!
接受连接请求的套接字创建过程
- 调用socket函数创建套接字
- 调用bind函数分配IP地址和端口号
- 调用listen函数转为可接收请求状态
- 调用accept函数受理连接请求
hello_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char* argv){
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
scoklen_t clnt_addr_size;
char message[] = "Hello World!";
if(argc!=2){
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0); // 1. 调用socket函数创建套接字
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr
=htonl(INADDR_ANY);
serv_addr.sin_port
=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1) // 2. 调用bind函数分配IP地址和端口号
error_handing("bind() error");
if(listen(serv_sock, 5)==-1) // 3. 调用listen函数将套接字转化为可接收连接状态
error_handling("listen() error");
clnt_adder_size=sizeof(clnt_addr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); // 4. 调用accept函数受理连接请求。如果没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止
if(clnt_sock==-1)
error_handling("accept() error");
write(clnt_sock, message, sizeof(message)); // 5. write函数用于传输数据,若程序经过第4布执行到本行,说明已经有了连接请求
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
hello_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char* argv[]){
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc!=3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0); // 1. 创建套接字,但此时套接字并不马上分为服务器端和客户端。如果紧接着调用bind、listen函数,将成为服务器端套接字; 如果调用connect函数,将成为客户端套接字
if(sock==-1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) // 2. 调用connect函数向服务器端发送连接请求
error_handling("connect() error!");
str_len=read(sock, message, sizeof(message)-1);
if(str_len==-1)
error_handling("read() error!");
printf("Message from servere : %s \n", message);
close(sock);
return 0;
}
void error_handling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行
gcc hello_server.c -o hserver 生成可执行文件hserver
./hserver 9190 运行当前目录下hserver文件
gcc hello_client.c -o hclient
./hclient 127.0.0.1 9190
Message from server: Hello World!
消息完成完后,服务器端和客户端都停止运行
再次运行程序前需等待
上面服务端无法立即重新运行。如果再次运行,需更改端口号。后面解释原因
基于Linux的文件操作
Linux中,socket也是一种文件,网络数据传输中可使用I/O相关函数
Windows和Linux不同,要区分socket和文件的,Windows需要调用特殊的数据传输相关函数
底层文件访问(Low-Lever File Access)和文件描述符(File Descriptor)
- 文件描述符是系统分配给文件或套接字的整数
分配给标准输入及标准错误的文件描述符:
文件和套接字要经过创建才被分配文件描述符,而上面三种无需创建程序开始运行就自动分配文件描述符
- Windows中叫“句柄”,Linux中叫“描述符”
- 文件打开、写入、读取、关闭
打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcnt1.h>
int open(const char *path, int flag);
成功返回文件描述符,失败返回-1
path: 文件名的字符串地址
flag: 文件打开模式信息
文件打开模式:
关闭文件
#include <unistd.h>
int close(int fd);
成功返回0, 失败返回-1
fd:需要关闭的文件或套字的文件描述符
将数据写入文件
#include <unistd.h>
ssize_t write(int fd, const void * buf, size_t nbytes);
成功返回写入的字节数,失败返回-1
fd:显示数据传输对象的文件描述符
buf:保存要传输数据的缓存地址值
nbytes:要传输数据的字节数
size_t通过typedef声明的unsigned int类型
ssize_t:size_t前面多加一个s表示是signed
ssize_t是通过typedef声明的signed int类型
元数据类型(primitive)
_t为后缀的数据类型,sys/types.h头文件中由typedef定义
不同操作系统int的位数不同,有32或16, 用size_t或ssize_t生命为四个字节的,后续代码如果修改就很方便,只需修改并编译size_t和typedef声明即可。
一般使用typedef和_t后缀声明基本数据类型的别名
例子:创建新文件并保存数据
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unsistd.h>
void error_handling(char* message)
int main(void){
int fd;
char buf[] = "Let's go!\n";
fd = open("data.txt", O_CREAT|O_WRONLY|O_TRUNC); // 1. 打开模式为这三个,将创建空文件,只能写。若存在data.txt,清空文件全部数据
if(fd == -1)
error_handling("open() error!"); // 2. 打开失败
printf("file descriptor: %d \n", fd);
if(write(fd, buf, sizeof(buf))==-1) // 3. 写入buf字符串到文件
error_handling("write() error!"); // 4. 写入文件失败处理
close(fd); // 5. 关闭文件
return 0;
}
void error_handling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行
gcc low_open.c -o lopen
./lopen
file descriptor: 3
cat data.txt
Let's go~
读取文件
#include <unistd.h>
ssize_t read(int fd, void * buf, size_t nbytes);
成功返回接收的字节数(遇到文件结尾返回0),失败返回-1
fd:显示数据接收对象的文件描述符
buf:要保存接收数据的缓存地址值
nbytes:要接收数据的最大字节数
***例子通过read函数读取data.txt保存的数据
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 100
void error_handling(char* message);
int main(void){
int fd;
char buf[BUF_SIZE];
fd=open("data.txt", O_RONLY); // 1. 打开读取专用文件data.txt
if(fd==-1)
error_handling("open() error!");
printf("file descriptor: %d \n", fd);
if(read(fd, buf, sizeof(buf)) == -1) // 2. 调用read向buf中保存读入的数据
error_handling("read() error!");
printf("file data: %s, buf");
close(fd);
return 0;
}
void error_handling(char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行
gcc low_read.c -o lread
./lread
file descriptor: 3
file data: Let's go!
文件描述符与套接字
fd_seri.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main(void){
int fd1, fd2, fd3;
fd1 = socket(PF_INET, SOCK_STREAM, 0); // 1. 创建1个文件和2个套接字
fd2 = open("test.dat", O_CREAT|O_WRONLY|O_TRUNC);
fd3 = socket(PF_INET, SOCK_DGRAM, 0);
printf("file descriptor 1: %d\n", fd1); // 输出创建的文件描述符的整数值
printf("file descriptor 2: %d\n", fd2);
printf("file descriptor 3: %d\n", fd3);
close(fd1); close(fd2); close(fd3);
return 0;
}
运行
gcc fd_seri.c -o fds
./fds
file descriptor 1: 3
file descriptor 2: 4
file descriptor 3: 5
0、 1、 2是分配给I/O的描述符,输出结果描述符是从小到大编号
windows系统实现
- Windows套接字头文件和库
- 导入头文件winsock2.h
- 链接ws2_32.lib库
VisualStudio 项目属性-链接器-输入-附加依赖项:
添加ws2_32.lib:
- Winsock初始化
#include <winsock2.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
成功返回0,失败返回非零的错误代码值
wVersionRequested:程序员要用的Winsock版本信息
lpWSAData:WSADATA结构体u变量的地址值
Winsock版本信息
WORD类型(typedef声明定义的unsigned short类型)套接字版本信息
wVersionRequested: 若版本号为1.2,则1是主版本号,2是副版本号,传递0x0201
高8位为副版本号,低8位为主版本号
本书使用2.2.版本,传递0x0202
借助MAKEWORD宏函数构建WORD型版本信息:
MAKEWORD(1, 2);: // 主版本为1,副版本为2,返回0x0201
MAKEWORD(2, 2);: // 主版本为2,副版本为2,返回0x0202
lpWSADATA(WSADATA型结构体变量地址)
int main(int argc, char* argv[]){
WSADATA wsaDATA;
....
if(WAStartup(MAKEWORD(2, 2), &wsaDATA) != 0)
ErrorHandling("WSAStartup() error!");
....
return 0;
}
调用完函数后,相应参数中填充已初始化的库信息
注销该库
#include <winsock2.h>
int WSACleanup(void);
成功返回0, 失败返回SOCKET_ERROR
调用该函数,Winsock相关库归还Windows操作系统,无法再调用Winsock相关函数。无需再使用Winsock函数时才调用,通常在程序结束之前调用
基于Windows套接字相关函数和实例
例子
hello_server_win.c
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
void ErrorHandling(char* message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAddr, clntAddr; // 服务端地址和客户端地址
int szClntAddr;
char message[] = "Hello World!";
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) // 初始化套接字库
ErrorHandling("WSAStartup() error!");
hServSock = socket(PF_INET, SOCK_STREAM, 0); // 成功时返回套接字句柄,失败时返回INVALID_SOCKET
if (hServSock == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
// 调用bind函数给套接字分配IP地址和端口号,成功时返回0,失败时返回SOCKET_ERROR
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("bind() error");
// 调用listen,使hServSock成为服务端套接字,成功时返回0,失败时返回SOCKET_ERROR
if (listen(hServSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
szClntAddr = sizeof(clntAddr);
// 调用accept函数受理客户端连接请求,成功时返回套接字句柄,失败时返回INVALID_SOCKET
hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
if (hClntSock == INVALID_SOCKET)
ErrorHandling("accept() error");
// 调用send向客户端发送数据
send(hClntSock, message, sizeof(message), 0);
closesocket(hClntSock); // 关闭套接字,成功时返回0,失败时返回SOCKET_ERROR
closesocket(hServSock);
WSACleanup(); // 程序终止前注销第20行中初始化的套接字库
return 0;
}
void ErrorHandling(char* message) {
fputs(message, stderr);
fputs((char*)'\n', stderr);
exit(1);
}
hello_client_win.c
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
void ErrorHandling(const char* message);
int main(int argc, char* argv[]) {
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[30];
int strLen;
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
// 初始化Winsock库
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
// 创建套字
hSocket = socket(PF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
//servAddr.sin_addr.s_addr = inet_pton(argv[1]); // 老版本函数
inet_pton(AF_INET, argv[1], (void*)&servAddr.sin_addr.s_addr);
servAddr.sin_port = htons(atoi(argv[2]));
// 调用connect通过hSocket套接字向服务器发送连接请求,成功时返回0,失败时返回SOCKET_ERROR
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
ErrorHandling("connect() error!");
// 调用recv函数接收服务器发来的数据
strLen = recv(hSocket, message, sizeof(message) - 1, 0);
if (strLen == 1)
ErrorHandling("read() error!");
printf("Message from server: %s \n", message);
closesocket(hSocket);
WSACleanup(); // 注销已初始化的Winsock库
return 0;
}
void ErrorHandling(const char* message) {
fputs(message, stderr);
fputs((char*)'\n', stderr);
exit(1);
}
运行
#启动服务端
c:\tcpip>hClientWin 127.0.0.1 9190 # hServerWin是生成的exe文件
Message from server: Hello World!
#启动客户端
c:\tcpip>hClientWin 127.0.0.1 9190
Message from server: Hello World!
Windows中文件句柄和套接字句柄
Linux内部也将套接字当作文件,创建文件和套接字都返回文件描述符
Windows通过调用系统函数创建文件,返回句柄(类似Linux 文件描述符),但Window区分文件句柄和套接字句柄
IO函数
- Linux IO函数
套接字也是文件,可以使用read和write函数读写
write(SOCKET s, const char* buf, int len)
Linux也有send和recv方法,也来自BSD套接字,作用和Windows的一样
- Windows IO函数
严格区分文件IO和套接字IO
send函数
send(SOCKET s, const char* buf, int len, int flags),与Linuxwrite比多一个flags参数,为0表示不设置任何选项
成功返回传输字节数,失败返回SOCKET_ERROR
s:表示数据传输对象连接的套接字句柄值
buf:保持待传输数据的缓冲地址值
len:要传输的字节数
flags:传输数据时用到的多种选项信息
send函数非windows独有,linux也有,也来自BSD套接字
recv函数
#include <winsock2.h>
int recv(SOCKET s, const char *buf, int len, int flags);
成功返回接收的字节数(收到EOF时返回0),失败返回SOCKET_ERROR
s:表示数据接收对象连接的套接字句柄值
buf:保存接收数据的缓冲地址值
len:能够接收的最大字节数
flags:接收数据时用到的多种选项信息