原本是想使用C语言搭建一个简易的HTTP服务器,但发现HTTP服务器搭建与采用套接字实现TCP通信之间就只差了规定的HTTP报文而已。因此用这篇博客梳理一下socket-tcp的编程过程:server/client模式,客户端向服务端发送一个字符串,服务端向客户端返回字符串的大写形式。
硬件环境:
sudo ufw allow 8080
。int serverfd = socket(AF_INET, SOCK_STREAM, 0) //创建服务器端套接字文件
在Linux中,所有硬件都已文件的形式存在,int serverfd 就代表服务器端的套接字。函数原型为:
int socket(int domain, int type, int protocol);
int domain
:选择通信时的协议族,常用设置为AF_INET(因特网,使用IPv4格式的IP地址)和AF_UNIX(本地进程)。
int type
:socket的连接类型。常见的SOCK_STREAM——TCP协议;SOCK_DGRAM——UDP协议。
int protocol
:0代表默认协议。
将套接字绑定至某一个特定的进程(即我们的服务端)。
bind(serverfd, (struct sockaddr *)&servaddr, sizeof(servaddr))
函数原型:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
int sockfd
:套接字的文件描述符,及我们采用socket函数创建的返回值。
const struct sockaddr *addr
:描述服务器端口地址的结构体。
socklen_t addrlen
:第二个参数addr的长度。
struct sockaddr这个东西定义有好几种形式(有点像C++中继承的关系):IPv4地址用结构体sockaddr_in表示,IPv6地址用结构体sockaddr_in6,这两个的地址格式定义在库文件netinet/in.h中。我们根据不同的情况创建 sockaddr_in 或者 sockaddr_in6,给bind函数传参时进行一下类型转换就OK了。也正是因为这个原因,才会有第三个参数输入结构体长度 addrlen 的必要。实现如下:
struct sockaddr_in servaddr
socklen_t cliaddr_len;
bzero(&servaddr, sizeof(servaddr)); // 结构体清零
servaddr.sin_family = AF_INET; // 设置地址类型为AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置网络地址为INADDR_ANY
servaddr.sin_port = htons(LISTENPORT); // 设置端口号
bind(serverfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
INADDR_ANY
表示服务器可以接受来自任意网络接口的连接请求;
htonl函数
:字节序转换函数,位于库arpa/inet.h
TCP/IP协议规定,网络数据流应采用大端模式存储。主机的存储可能是大端也可能是小端。因此需要将用于将主机字节序(Host Byte Order)转换为网络字节序(Network Byte Order),以确保在不同字节序的系统上都能正常工作。函数原型如下:
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
listen(serverfd, MAXLISTENNUM);
使已绑定的socket监听对应客户端进程状态,并同时设置服务器同时可建立的连接的数量。
至此,服务器的服务已经开启,只等待客户端发起连接。
若要实现一直监听客户端请求,以下步骤放进while循环中。
clientfd = accept(serverfd, (struct sockaddr *)&clientaddr, &cliaddr_len);
ssize_t byteRead = recv(clientfd, buffer, BUFFSIZE, 0);
ssize_t byteSend = send(clientfd, buffer, byteRead, 0);
close(clientfd);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
struct sockaddr *addr
是传出参数(待被填充的数组),用于存储客户端的信息。参数addrlen
是一个传入传出参数,传入时为函数调用者提供的缓冲区addr的长度,传出时为客户端地址结构体的实际长度。ssize_t recv(int sockfd, void *buf, size_t len, int flags);
int sockfd
代表从哪个文件描述符获得数据,即由accept
获得的客户端套接字。void *buf
与size_t len
分别指接收信息存放的数组及其大小。int flags
表示调用的执行方式(阻塞/非阻塞)。ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv
函数类似,void *buf
与size_t len
分别指发送信息存放的数组及其大小。int close(int fd);
补充:
inet_ntoa(clientaddr.sin_addr)
和ntohs(clientaddr.sin_port)
,用于将IP地址格式转换为二进制格式。#include
#include
#include
#include
#include
#include
#include
#include
#define LISTENPORT 8080
#define MAXLISTENNUM 10
#define BUFFSIZE 1024
int main()
{
struct sockaddr_in servaddr,clientaddr; //Ipv4
socklen_t cliaddr_len;
int serverfd;
int clientfd;
char buffer[BUFFSIZE];
/*1.创建服务器端套接字文件*/
if((serverfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
fprintf(stderr,"SERVER:create server socket error");
return -1;
}
/*2.初始化服务器端口地址*/
bzero(&servaddr, sizeof(servaddr)); // 结构体清零
servaddr.sin_family = AF_INET; // 设置地址类型为AF_INET
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置网络地址为INADDR_ANY
servaddr.sin_port = htons(LISTENPORT); // 设置端口号
/*3.将套接字文件与服务器端口地址绑定*/
if(bind(serverfd, (struct sockaddr *)&servaddr, sizeof(servaddr))==-1){
fprintf(stderr,"SERVER:bind server socket error");
return -1;
}
/*4.监听并设置最大连接数*/
if(listen(serverfd, MAXLISTENNUM)==-1){
fprintf(stderr,"SERVER:listen on socket error");
return -1;
}
printf("SERVER:Server started. Listening on port %d...\n", LISTENPORT);
while (1)
{
cliaddr_len = sizeof(clientaddr);
if((clientfd = accept(serverfd, (struct sockaddr *)&clientaddr, &cliaddr_len)) == -1){
fprintf(stderr,"SERVER:accept on socket error");
return -1;
}
ssize_t byteRead = recv(clientfd, buffer, BUFFSIZE, 0);
if(byteRead == -1){
fprintf(stderr,"SERVER:receive socket content error");
return -1;
}
printf("SERVER:received from %s at PORT %d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
for(int i=0;i<byteRead;i++){
buffer[i] = toupper(buffer[i]);
}
ssize_t byteSend = send(clientfd, buffer, byteRead, 0);
if(byteSend == -1){
fprintf(stderr,"SERVER:send content to socket error");
return -1;
}
close(clientfd);
}
return 0;
}
客户端和服务端大多数套接字使用的接口相同。
在windows中,不采用
#include
#pragma comment(lib, "ws2_32.lib")
并且在程序启动最开始运行以初始化Winsock库。
WORD sockVersion = MAKEWORD(2, 2);
WSADATA data;
if (WSAStartup(sockVersion, &data) != 0) // Windows异步套接字的启动命令
{
fprintf(stderr, "CLIENT:Failed to initialize Winsock Error.\n");
return -1;
}
struct sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(8080);
serAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.89");
需要设置服务器的监听端口号和服务器的IP地址。
#include
#include
#include
#include
#pragma comment(lib, "ws2_32.lib")
#include
#define LISTENPORT 8080
#define BUFFSIZE 1024
int main(int argc, char *argv[])
{
char recData[BUFFSIZE];
/*1.初始化Winsock库*/
WORD sockVersion = MAKEWORD(2, 2);
WSADATA data;
if (WSAStartup(sockVersion, &data) != 0) // Windows异步套接字的启动命令
{
fprintf(stderr, "CLIENT:Failed to initialize Winsock Error.\n");
return -1;
}
if (argc != 2)
{
fprintf(stderr, "CLIENT:Number of Program Input Error\n");
return -1;
}
char *str = argv[1];
/*2.创建套接字 */
SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sclient == INVALID_SOCKET)
{
fprintf(stderr, "CLIENT:Create Socket Error.\n");
return 0;
}
/*3.初始化服务器端口地址*/
struct sockaddr_in serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(8080);
serAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.89");
/*4.请求链接*/
if (connect(sclient, (struct sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR) // 连接
{
fprintf(stderr, "CLIENT:Connect Server Error\n");
closesocket(sclient);
return 0;
}
/*5.发送数据*/
ssize_t byteSend = send(sclient, str, strlen(str), 0);
/*6.接收客户端返回的数据*/
ssize_t byteRead = recv(sclient, recData, BUFFSIZE , 0);
printf("Response from server:%s\n", recData);
/*7.关闭连接*/
closesocket(sclient);
WSACleanup(); // 功能是终止Winsock 2 DLL (Ws2_32.dll) 的使用
return 0;
}
编译的时候记得加上 -lwsock32。
#include
#include
#include
#include
#include
#include
#define SERVEPORT 8080
#define BUFFSIZE 1024
int main(int argc, char *argv[]){ //其中argv[0]指向程序名称本身,argv[1]指向第一个命令行参数
int clientfd;
struct sockaddr_in servaddr;
socklen_t addrlen;
char buffer[BUFFSIZE];
if(argc != 2){
fprintf(stderr,"CLIENT:number of program input error");
return -1;
}
char* str = argv[1];
/*1.创建服务器端套接字文件*/
if((clientfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
fprintf(stderr,"CLIENT:create client socket error");
return -1;
}
/*2.初始化服务器端口地址*/
bzero(&servaddr, sizeof(servaddr)); // 结构体清零
servaddr.sin_family = AF_INET; // 设置地址类型为AF_INET
inet_pton(AF_INET, "192.168.1.89", &servaddr.sin_addr); //设置目的IP
servaddr.sin_port = htons(SERVEPORT); // 设置目的端口号
/*3.请求链接*/
if(connect(clientfd, (struct sockaddr *)&servaddr,sizeof(servaddr)) == -1){
fprintf(stderr,"CLIENT:connect server error");
return -1;
}
/*4.发送数据*/
ssize_t byteSend = send(clientfd, str, strlen(str), 0);
/*5.接收客户端返回的数据*/
ssize_t byteRead = recv(clientfd, buffer, BUFFSIZE, 0);
printf("Response from server:%s\n",buffer);
/*6.关闭连接*/
close(clientfd);
return 0;
}
可参考的链接:
https://book.itheima.net/course/223/1277519158031949826/1277528764959432706